mirror of https://github.com/cirruslabs/tart.git
fix(save): use standard OCI layer format for skopeo compatibility
Replace custom Cirrus Labs layer media types (tart.config.v1, tart.disk.v2, tart.nvram.v1) with a single standard application/vnd.oci.image.layer.v1.tar+gzip layer containing the VM files as a tar+gzip archive. This makes the archive fully compatible with skopeo, Docker, and any OCI-compliant tool, since all layers use standard media types. The OCI config labels still carry tart-specific metadata (disk format, etc.) for round-tripping.
This commit is contained in:
parent
392462ec10
commit
9207c5ec6b
|
|
@ -8,6 +8,9 @@ let ociConfigMediaType = "application/vnd.oci.image.config.v1+json"
|
|||
let dockerManifestMediaType = "application/vnd.docker.distribution.manifest.v2+json"
|
||||
let dockerConfigMediaType = "application/vnd.docker.container.image.v1+json"
|
||||
|
||||
// Standard OCI layer media type
|
||||
let ociLayerMediaType = "application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
|
||||
// Layer media types
|
||||
let configMediaType = "application/vnd.cirruslabs.tart.config.v1"
|
||||
let diskV2MediaType = "application/vnd.cirruslabs.tart.disk.v2"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
class OCIArchiveWriter {
|
||||
private let tmpDir: URL
|
||||
let tmpDir: URL
|
||||
private let blobsDir: URL
|
||||
private let lock: FileLock
|
||||
private var manifestDigest: String?
|
||||
|
|
@ -72,7 +72,7 @@ extension OCIArchiveWriter: BlobStorage {
|
|||
var manifests: [[String: Any]] = []
|
||||
|
||||
let baseDescriptor: [String: Any] = [
|
||||
"mediaType": dockerManifestMediaType,
|
||||
"mediaType": ociManifestMediaType,
|
||||
"digest": manifestDigest,
|
||||
"size": manifestSize,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,42 +4,53 @@ extension VMDirectory {
|
|||
func saveToArchive(path: String, concurrency: UInt, labels: [String: String] = [:], tag: String? = nil) async throws {
|
||||
let archive = try OCIArchiveWriter()
|
||||
|
||||
var layers = [OCIManifestLayer]()
|
||||
let diskSize = try FileManager.default.attributesOfItem(atPath: diskURL.path)[.size] as! Int64
|
||||
|
||||
// Create a standard tar+gzip layer containing VM files
|
||||
defaultLogger.appendNewLine("archiving disk... this will take a while...")
|
||||
let progress = Progress(totalUnitCount: diskSize)
|
||||
ProgressObserver(progress).log(defaultLogger)
|
||||
|
||||
let layerTarGz = archive.tmpDir.appendingPathComponent("layer.tar.gz")
|
||||
|
||||
let tarProcess = Process()
|
||||
tarProcess.executableURL = URL(fileURLWithPath: "/usr/bin/tar")
|
||||
tarProcess.arguments = ["-czf", layerTarGz.path, "-C", baseURL.path,
|
||||
"disk.img", "nvram.bin", "config.json"]
|
||||
|
||||
let tarPipe = Pipe()
|
||||
tarProcess.standardError = tarPipe
|
||||
|
||||
try tarProcess.run()
|
||||
tarProcess.waitUntilExit()
|
||||
|
||||
if tarProcess.terminationStatus != 0 {
|
||||
let errorData = tarPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
throw RuntimeError.Generic(
|
||||
"creating archive layer failed: \(String(data: errorData, encoding: .utf8) ?? "unknown error")"
|
||||
)
|
||||
}
|
||||
|
||||
let layerData = try Data(contentsOf: layerTarGz, options: .alwaysMapped)
|
||||
let layerDigest = try await archive.pushBlob(fromData: layerData, chunkSizeMb: 0, digest: nil)
|
||||
progress.completedUnitCount = diskSize
|
||||
|
||||
let config = try VMConfig(fromURL: configURL)
|
||||
var labels = labels
|
||||
labels[diskFormatLabel] = config.diskFormat.rawValue
|
||||
let configJSON = try JSONEncoder().encode(config)
|
||||
defaultLogger.appendNewLine("saving config...")
|
||||
let configDigest = try await archive.pushBlob(fromData: configJSON, chunkSizeMb: 0, digest: nil)
|
||||
layers.append(OCIManifestLayer(mediaType: configMediaType, size: configJSON.count, digest: configDigest))
|
||||
|
||||
let diskSize = try FileManager.default.attributesOfItem(atPath: diskURL.path)[.size] as! Int64
|
||||
defaultLogger.appendNewLine("saving 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: archive, chunkSizeMb: 0, concurrency: concurrency, progress: progress))
|
||||
|
||||
defaultLogger.appendNewLine("saving NVRAM...")
|
||||
let nvram = try FileHandle(forReadingFrom: nvramURL).readToEnd()!
|
||||
let nvramDigest = try await archive.pushBlob(fromData: nvram, chunkSizeMb: 0, digest: nil)
|
||||
layers.append(OCIManifestLayer(mediaType: nvramMediaType, size: nvram.count, digest: nvramDigest))
|
||||
|
||||
let ociConfigContainer = OCIConfig.ConfigContainer(Labels: labels)
|
||||
let ociConfigJSON = try OCIConfig(architecture: config.arch, os: config.os, config: ociConfigContainer).toJSON()
|
||||
let ociConfigDigest = try await archive.pushBlob(fromData: ociConfigJSON, chunkSizeMb: 0, digest: nil)
|
||||
|
||||
var manifestConfig = OCIManifestConfig(size: ociConfigJSON.count, digest: ociConfigDigest)
|
||||
manifestConfig.mediaType = dockerConfigMediaType
|
||||
|
||||
var manifest = OCIManifest(
|
||||
config: manifestConfig,
|
||||
layers: layers,
|
||||
let manifest = OCIManifest(
|
||||
config: OCIManifestConfig(size: ociConfigJSON.count, digest: ociConfigDigest),
|
||||
layers: [
|
||||
OCIManifestLayer(mediaType: ociLayerMediaType, size: layerData.count, digest: layerDigest)
|
||||
],
|
||||
uncompressedDiskSize: UInt64(diskSize),
|
||||
uploadDate: Date()
|
||||
)
|
||||
manifest.mediaType = dockerManifestMediaType
|
||||
|
||||
let tagRef = tag ?? "latest"
|
||||
defaultLogger.appendNewLine("saving manifest...")
|
||||
|
|
|
|||
Loading…
Reference in New Issue