containerization-framework 0.2.0

Rust bindings for Apple's Containerization framework: Linux containers in lightweight VMs, in-process and without a daemon.
//===----------------------------------------------------------------------===//
// Putting a kernel and an init image in the store.
//
// A VM needs a kernel to boot and an init image to boot into. Both are fetched:
//
//   * the init image (`vminitd`, the agent the library talks to over vsock) is
//     an OCI image, pulled through ImageStore;
//   * the kernel is not published as an image, so it comes from Kata
//     Containers' static release, as in Containerization's Makefile and the
//     CLI's first-start offer.
//
// Idempotent and cheap when there is nothing to do, so `build` calls it every
// time.
//===----------------------------------------------------------------------===//

import Containerization
import ContainerizationArchive
import ContainerizationOCI
import Foundation
import SystemPackage

struct ProvisionSpec: Sendable {
    var storeRoot: String
    /// Where the kernel is downloaded to, if nothing is there.
    var kernelPath: String
    var kernelURL: String
    /// The kernel's path inside the downloaded archive.
    var kernelInArchive: String
    var initfsReference: String
    /// Where the init image is unpacked to, if nothing is there.
    var initfsPath: String
}

enum Provision {
    static func run(_ spec: ProvisionSpec) async throws {
        let root = URL(filePath: spec.storeRoot)
        try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)

        async let kernel: Void = kernel(spec)
        async let initfs: Void = initfs(spec, root: root)

        _ = try await (kernel, initfs)
    }

    /// Downloads and unpacks the kernel, unless it is already there.
    private static func kernel(_ spec: ProvisionSpec) async throws {
        let destination = URL(filePath: spec.kernelPath)

        guard !FileManager.default.fileExists(atPath: destination.path(percentEncoded: false)) else {
            return
        }

        guard let url = URL(string: spec.kernelURL) else {
            throw BridgeError.malformed("kernel url", spec.kernelURL)
        }

        note("downloading a kernel from \(url.absoluteString)")

        // To a file, not memory: the archive is hundreds of megabytes and only
        // one entry is wanted.
        let (archive, response) = try await URLSession.shared.download(from: url)

        defer { try? FileManager.default.removeItem(at: archive) }

        if let status = (response as? HTTPURLResponse)?.statusCode, status != 200 {
            throw BridgeError.kernelUnavailable(url.absoluteString, status)
        }

        note("unpacking \(spec.kernelInArchive)")

        let binary = try extract(spec.kernelInArchive, from: archive)

        try FileManager.default.createDirectory(
            at: destination.deletingLastPathComponent(),
            withIntermediateDirectories: true
        )
        // Written aside and moved, so an interrupted provision never leaves a
        // half-written kernel that looks complete.
        let partial = destination.appendingPathExtension("partial")
        try binary.write(to: partial, options: .atomic)
        _ = try FileManager.default.replaceItemAt(destination, withItemAt: partial)
    }

    /// Reads one file out of the archive, following a symlink: in Kata's release
    /// `vmlinux.container` links to the versioned kernel beside it, and a link
    /// entry has no contents.
    private static func extract(_ path: String, from archive: URL) throws -> Data {
        let (entry, data) = try ArchiveReader(file: archive).extractFile(path: path)

        guard entry.fileType == .symbolicLink, let target = entry.symlinkTarget else {
            guard !data.isEmpty else {
                throw BridgeError.kernelMissing(path)
            }

            return data
        }

        // Resolved against the link's directory (`pushing` replaces outright
        // when the target is absolute). A fresh reader, because extracting moved
        // the old one past entries the target may precede.
        let resolved = FilePath(path).removingLastComponent().pushing(FilePath(target)).string
        let (_, contents) = try ArchiveReader(file: archive).extractFile(path: resolved)

        guard !contents.isEmpty else {
            throw BridgeError.kernelMissing(resolved)
        }

        return contents
    }

    /// Pulls and unpacks the init image, skipping whichever is done.
    ///
    /// `getInitImage` pulls on its own; the check exists to log first, since the
    /// pull is the slow part of a first build and a silent wait looks like a
    /// hang.
    private static func initfs(_ spec: ProvisionSpec, root: URL) async throws {
        let imageStore = try ImageStore(path: root)

        if (try? await imageStore.get(reference: spec.initfsReference)) == nil {
            note("pulling \(spec.initfsReference)")
        }

        _ = try await Initfs.mount(spec.initfsReference, at: URL(filePath: spec.initfsPath), in: imageStore)
    }
}

/// The init image, unpacked to the caller's path, for
/// `ContainerManager(initfs:)`. What `ContainerManager(initfsReference:)`
/// does, minus its fixed `initfs.ext4`.
enum Initfs {
    /// The image at `path`, read-only; unpacked first if nothing is there.
    static func mount(
        _ reference: String,
        at path: URL,
        in imageStore: ImageStore
    ) async throws -> Containerization.Mount {
        if !FileManager.default.fileExists(atPath: path.path(percentEncoded: false)) {
            try await unpack(reference, to: path, in: imageStore)
        }

        return .block(format: "ext4", source: path.absolutePath(), destination: "/", options: ["ro"])
    }

    /// Unpacked aside and moved into place: no half-written file, no sharing
    /// between concurrent first boots.
    private static func unpack(_ reference: String, to path: URL, in imageStore: ImageStore) async throws {
        let directory = path.deletingLastPathComponent()
        try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)

        let image = try await imageStore.getInitImage(reference: reference)
        let partial = directory.appending(path: "\(UUID().uuidString).partial")
        defer { try? FileManager.default.removeItem(at: partial) }

        _ = try await image.initBlock(at: partial, for: .linuxArm)

        do {
            try FileManager.default.moveItem(at: partial, to: path)
        } catch where FileManager.default.fileExists(atPath: path.path(percentEncoded: false)) {
            // Another boot unpacked it first; its copy is equivalent.
        }
    }
}