containerization-framework 0.2.2

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)
  }
}