tart/Tests/TartTests/VMStorageOCITests.swift

392 lines
15 KiB
Swift

import Foundation
import XCTest
@testable import tart
#if canImport(DiskImageKit)
import DiskImageKit
#endif
final class VMStorageOCITests: XCTestCase {
func testPopulateStandalonePushedImageCachesDiskAndManifest() throws {
try withTemporaryTartHome {
let source = try standaloneSource(diskData: Data("disk".utf8))
let manifest = try flatManifest()
let name = try digestName(for: manifest)
let storage = try VMStorageOCI()
try storage.populate(name, from: source, manifest: manifest)
let cached = try storage.open(name)
XCTAssertTrue(cached.isStandalone)
XCTAssertEqual(try Data(contentsOf: cached.diskURL), Data("disk".utf8))
XCTAssertEqual(try OCIManifest(fromJSON: Data(contentsOf: cached.manifestURL)), manifest)
}
}
func testStackedCloneRequiresManifestForLegacyStandaloneCachedImage() throws {
try withTemporaryTartHome {
let manifest = try flatManifest()
let name = try digestName(for: manifest)
let storage = try VMStorageOCI()
let record = try storage.create(name)
try config().save(toURL: record.configURL)
XCTAssertTrue(FileManager.default.createFile(atPath: record.nvramURL.path, contents: Data()))
XCTAssertTrue(FileManager.default.createFile(atPath: record.diskURL.path, contents: Data()))
XCTAssertTrue(try storage.hasUsableCachedImageForClone(name))
XCTAssertFalse(try storage.hasUsableCachedImageForClone(name, requireManifest: true))
}
}
func testCloneCacheCheckRejectsMissingOrWrongSizedStackedContent() throws {
try withTemporaryTartHome {
let baseData = Data("base".utf8)
let overlayData = Data("overlay".utf8)
let baseDigest = Digest.hash(baseData)
let overlayDigest = Digest.hash(overlayData)
let manifest = try stackedManifest(
baseContentDigest: baseDigest,
overlayContentDigest: overlayDigest,
baseUncompressedSize: UInt64(baseData.count),
overlayUncompressedSize: UInt64(overlayData.count)
)
let name = try digestName(for: manifest)
let storage = try VMStorageOCI()
let record = try storage.create(name)
try config().save(toURL: record.configURL)
XCTAssertTrue(FileManager.default.createFile(atPath: record.nvramURL.path, contents: Data()))
try manifest.toJSON().write(to: record.manifestURL)
XCTAssertFalse(try storage.hasUsableCachedImageForClone(name))
let contentStore = try ContentStore()
try installContent(baseData, contentDigest: baseDigest, into: contentStore)
try installContent(overlayData, contentDigest: overlayDigest, into: contentStore)
XCTAssertTrue(try storage.hasUsableCachedImageForClone(name))
try Data("bad".utf8).write(to: try contentStore.contentURL(for: overlayDigest))
XCTAssertFalse(try storage.hasUsableCachedImageForClone(name))
}
}
func testListIncludesStackedCachedImage() throws {
try withTemporaryTartHome {
let manifest = try stackedManifest()
let name = try digestName(for: manifest)
let storage = try VMStorageOCI()
let record = try storage.create(name)
try config().save(toURL: record.configURL)
XCTAssertTrue(FileManager.default.createFile(atPath: record.nvramURL.path, contents: Data()))
try manifest.toJSON().write(to: record.manifestURL)
XCTAssertTrue(try storage.list().contains { $0.0 == name.description })
XCTAssertEqual(try record.diskSizeBytes(), 4096)
XCTAssertNoThrow(try record.allocatedSizeBytes())
}
}
func testStackedCacheHitRequiresVerifiedContentAndSizesMissingFiles() throws {
try withTemporaryTartHome {
let baseData = Data("base".utf8)
let overlayData = Data("overlay".utf8)
let baseDigest = Digest.hash(baseData)
let overlayDigest = Digest.hash(overlayData)
let manifest = try stackedManifest(
baseContentDigest: baseDigest,
overlayContentDigest: overlayDigest,
baseUncompressedSize: 10,
overlayUncompressedSize: 20
)
let name = try digestName(for: manifest)
let storage = try VMStorageOCI()
let record = try storage.create(name)
try config().save(toURL: record.configURL)
XCTAssertTrue(FileManager.default.createFile(atPath: record.nvramURL.path, contents: Data()))
try manifest.toJSON().write(to: record.manifestURL)
XCTAssertFalse(try storage.hasCompleteCachedImage(name, manifest: manifest))
XCTAssertEqual(try storage.requiredDiskStorageBytes(for: manifest), 30)
let contentStore = try ContentStore()
try installContent(baseData, contentDigest: baseDigest, into: contentStore)
XCTAssertFalse(try storage.hasCompleteCachedImage(name, manifest: manifest))
XCTAssertEqual(try storage.requiredDiskStorageBytes(for: manifest), 20)
try installContent(overlayData, contentDigest: overlayDigest, into: contentStore)
XCTAssertTrue(try storage.hasCompleteCachedImage(name, manifest: manifest))
XCTAssertEqual(try storage.requiredDiskStorageBytes(for: manifest), 0)
let overlayURL = try contentStore.contentURL(for: overlayDigest)
try Data("corrupt".utf8).write(to: overlayURL)
XCTAssertFalse(try storage.hasCompleteCachedImage(name, manifest: manifest))
XCTAssertEqual(try storage.requiredDiskStorageBytes(for: manifest), 20)
}
}
func testStackedPullReusesPreviouslyPulledStandaloneDisk() throws {
try withTemporaryTartHome {
let diskData = Data([0])
let contentDigest = Digest.hash(diskData)
let flatManifest = try flatManifest()
let flatName = try digestName(for: flatManifest)
let storage = try VMStorageOCI()
let flatRecord = try storage.create(flatName)
try config().save(toURL: flatRecord.configURL)
XCTAssertTrue(FileManager.default.createFile(atPath: flatRecord.nvramURL.path, contents: Data()))
try diskData.write(to: flatRecord.diskURL)
try flatManifest.toJSON().write(to: flatRecord.manifestURL)
let stackedManifest = try stackedManifest(baseContentDigest: contentDigest)
XCTAssertNil(try ContentStore().existingContentURL(for: contentDigest))
try storage.reuseStandaloneDiskForStackedBaseIfPossible(stackedManifest)
let reusedURL = try XCTUnwrap(try ContentStore().existingContentURL(for: contentDigest))
XCTAssertEqual(try Data(contentsOf: reusedURL), diskData)
}
}
func testStackedPullDoesNotRehashInstalledBaseBeforeReuse() throws {
try withTemporaryTartHome {
let contentDigest = Digest.hash(Data("base".utf8))
let manifest = try stackedManifest(baseContentDigest: contentDigest)
let contentURL = try ContentStore().contentURL(for: contentDigest)
// Hashing this path would throw. Once an entry is published, this
// fast path must trust its presence and let normal pull validation
// repair unusable content later.
try FileManager.default.createDirectory(at: contentURL, withIntermediateDirectories: false)
XCTAssertNoThrow(try VMStorageOCI().reuseStandaloneDiskForStackedBaseIfPossible(manifest))
}
}
func testNewTagDoesNotValidateCachedStackBeforeLock() throws {
try withTemporaryTartHome {
let baseDigest = Digest.hash(Data("base".utf8))
let overlayDigest = Digest.hash(Data("overlay".utf8))
let manifest = try stackedManifest(
baseContentDigest: baseDigest,
overlayContentDigest: overlayDigest
)
let digestName = try digestName(for: manifest)
let tagName = RemoteName(
host: digestName.host,
namespace: digestName.namespace,
reference: Reference(tag: "latest")
)
let storage = try VMStorageOCI()
let record = try storage.create(digestName)
try config().save(toURL: record.configURL)
XCTAssertTrue(FileManager.default.createFile(atPath: record.nvramURL.path, contents: Data()))
try manifest.toJSON().write(to: record.manifestURL)
// Hashing this directory as a disk file throws. A new tag must skip
// validation until after it has taken the host lock.
let contentURL = try ContentStore().contentURL(for: baseDigest)
try FileManager.default.createDirectory(at: contentURL, withIntermediateDirectories: false)
XCTAssertFalse(try storage.hasCompleteLinkedImage(tagName, digestName: digestName, manifest: manifest))
}
}
func testStandaloneLayerCacheIgnoresStackedCachedImages() async throws {
try await withTemporaryTartHome {
var targetManifest = try flatManifest()
var stackedCandidateManifest = try stackedManifest()
let sharedDiskSize = 2 * 1024 * 1024 * 1024
targetManifest.layers[1].size = sharedDiskSize
stackedCandidateManifest.layers[1] = targetManifest.layers[1]
let candidateName = try digestName(for: stackedCandidateManifest)
let storage = try VMStorageOCI()
let candidate = try storage.create(candidateName)
try config().save(toURL: candidate.configURL)
XCTAssertTrue(FileManager.default.createFile(atPath: candidate.nvramURL.path, contents: Data()))
try stackedCandidateManifest.toJSON().write(to: candidate.manifestURL)
let targetName = RemoteName(
host: "example.com",
namespace: "org/target",
reference: Reference(digest: try targetManifest.digest())
)
let registry = try Registry(host: targetName.host, namespace: targetName.namespace)
let layerCache = try await storage.chooseLocalLayerCache(targetName, targetManifest, registry)
XCTAssertNil(layerCache)
}
}
#if canImport(DiskImageKit)
@available(macOS 27.0, *)
func testPopulateStackedPushedImageCachesImmutableTopOverlay() throws {
if #unavailable(macOS 27.0) {
throw XCTSkip("DiskImageKit tests require macOS 27 or newer")
}
try withTemporaryTartHome {
let source = try diskImageSource()
let stacked = try temporaryVMDirectory()
try source.cloneAsStackedBase(to: stacked, generateMAC: false)
var manifest = try OCIManifest(fromJSON: Data(contentsOf: stacked.manifestURL))
let contentDigest = try Digest.hash(stacked.overlayURL)
var overlay = OCIManifestLayer(
mediaType: asifOverlayMediaType,
size: 1,
digest: "sha256:overlay-transport",
uncompressedSize: 1,
uncompressedContentDigest: "sha256:overlay-chunk"
)
overlay.annotations?[diskFileContentDigestAnnotation] = contentDigest
overlay.annotations?[diskFileChunkCountAnnotation] = "1"
manifest.layers.insert(overlay, at: manifest.layers.count - 1)
let name = try digestName(for: manifest)
let storage = try VMStorageOCI()
try storage.populate(name, from: stacked, manifest: manifest)
let cached = try storage.open(name)
XCTAssertTrue(cached.isStackedCachedImage)
XCTAssertFalse(FileManager.default.fileExists(atPath: cached.overlayURL.path))
XCTAssertEqual(try OCIManifest(fromJSON: Data(contentsOf: cached.manifestURL)), manifest)
let cachedContent = try XCTUnwrap(try ContentStore().existingContentURL(for: contentDigest))
XCTAssertEqual(try Digest.hash(cachedContent), contentDigest)
}
}
#endif
private func standaloneSource(diskData: Data) throws -> VMDirectory {
let vmDir = try temporaryVMDirectory()
try config().save(toURL: vmDir.configURL)
XCTAssertTrue(FileManager.default.createFile(atPath: vmDir.nvramURL.path, contents: Data()))
try diskData.write(to: vmDir.diskURL)
return vmDir
}
#if canImport(DiskImageKit)
@available(macOS 27.0, *)
private func diskImageSource() throws -> VMDirectory {
let vmDir = try temporaryVMDirectory()
try config().save(toURL: vmDir.configURL)
XCTAssertTrue(FileManager.default.createFile(atPath: vmDir.nvramURL.path, contents: Data()))
_ = try DiskImage(creating: .raw(url: vmDir.diskURL, blockCount: 8))
try flatManifest().toJSON().write(to: vmDir.manifestURL)
return vmDir
}
#endif
private func config() -> VMConfig {
VMConfig(
platform: Linux(),
cpuCountMin: 2,
memorySizeMin: 512 * 1024 * 1024,
diskFormat: .raw
)
}
private func flatManifest() throws -> OCIManifest {
let disk = OCIManifestLayer(
mediaType: diskV2MediaType,
size: 1,
digest: "sha256:disk-transport",
uncompressedSize: 1,
uncompressedContentDigest: "sha256:disk-chunk"
)
return OCIManifest(
config: OCIManifestConfig(size: 1, digest: "sha256:oci-config"),
layers: [
OCIManifestLayer(mediaType: configMediaType, size: 1, digest: "sha256:config"),
disk,
OCIManifestLayer(mediaType: nvramMediaType, size: 1, digest: "sha256:nvram"),
]
)
}
private func stackedManifest(
baseContentDigest: String = "sha256:base",
overlayContentDigest: String = "sha256:overlay",
baseUncompressedSize: UInt64 = 1,
overlayUncompressedSize: UInt64 = 1
) throws -> OCIManifest {
var manifest = try flatManifest()
manifest.annotations?[diskBlockSizeAnnotation] = "512"
manifest.annotations?[uncompressedDiskSizeAnnotation] = "4096"
manifest.layers[1].annotations?[diskFileContentDigestAnnotation] = baseContentDigest
manifest.layers[1].annotations?[uncompressedSizeAnnotation] = String(baseUncompressedSize)
var overlay = OCIManifestLayer(
mediaType: asifOverlayMediaType,
size: 1,
digest: "sha256:overlay-transport",
uncompressedSize: overlayUncompressedSize,
uncompressedContentDigest: "sha256:overlay-chunk"
)
overlay.annotations?[diskFileContentDigestAnnotation] = overlayContentDigest
overlay.annotations?[diskFileChunkCountAnnotation] = "1"
manifest.layers.insert(overlay, at: manifest.layers.count - 1)
return manifest
}
private func installContent(_ data: Data, contentDigest: String, into contentStore: ContentStore) throws {
let temporaryURL = try contentStore.temporaryContentURL(for: contentDigest)
try data.write(to: temporaryURL)
_ = try contentStore.install(temporaryURL, contentDigest: contentDigest)
}
private func digestName(for manifest: OCIManifest) throws -> RemoteName {
RemoteName(
host: "example.com",
namespace: "org/image",
reference: Reference(digest: try manifest.digest())
)
}
private func withTemporaryTartHome(_ body: () throws -> Void) throws {
let home = try temporaryDirectory()
let previousHome = ProcessInfo.processInfo.environment["TART_HOME"]
setenv("TART_HOME", home.path, 1)
defer {
if let previousHome {
setenv("TART_HOME", previousHome, 1)
} else {
unsetenv("TART_HOME")
}
}
try body()
}
private func withTemporaryTartHome(_ body: () async throws -> Void) async throws {
let home = try temporaryDirectory()
let previousHome = ProcessInfo.processInfo.environment["TART_HOME"]
setenv("TART_HOME", home.path, 1)
defer {
if let previousHome {
setenv("TART_HOME", previousHome, 1)
} else {
unsetenv("TART_HOME")
}
}
try await body()
}
private func temporaryVMDirectory() throws -> VMDirectory {
VMDirectory(baseURL: try temporaryDirectory())
}
private func temporaryDirectory() throws -> URL {
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false)
addTeardownBlock {
try? FileManager.default.removeItem(at: url)
}
return url
}
}