containerization-framework 0.4.0

Rust bindings for Apple's Containerization framework: Linux containers in lightweight VMs, in-process and without a daemon.
//===----------------------------------------------------------------------===//
// `LinuxPod`, from Containerization.
//===----------------------------------------------------------------------===//

import Containerization
import ContainerizationOS
import Foundation

/// `configuration` is what Rust's closure made of `LinuxPod.Configuration()`,
/// which Swift's closure takes.
func linuxPod(
  id: RustStr,
  vmm: CzVirtualMachineManager,
  vmCpus: UInt32,
  vmMemoryInBytes: UInt64,
  configuration: RustPodConfiguration
) -> CzOutcome {
  let id = id.toString()

  return CzOutcome {
    let configuration = try LinuxPod.Configuration(configuration)

    return CzLinuxPod(
      try LinuxPod(id, vmm: vmm.manager, vm: VMResources(cpus: Int(vmCpus), memoryInBytes: vmMemoryInBytes)) {
        $0 = configuration
      }
    )
  }
}

final class CzLinuxPod: Sendable {
  let pod: LinuxPod

  init(_ pod: LinuxPod) {
    self.pod = pod
  }

  func id() -> String {
    pod.id
  }

  func config(seed: RustPodConfiguration, receive: RustConfigurePod) {
    fill(seed, from: pod.config)
    receive.call(seed)
  }

  func vmCpus() -> UInt32 {
    UInt32(pod.vm.cpus)
  }

  func vmMemoryInBytes() -> UInt64 {
    pod.vm.memoryInBytes
  }

  /// `seed` is Rust's `ContainerConfiguration()`, which is Swift's, for
  /// `configuration` to change: Swift never fills one.
  func addContainer(
    id: RustStr,
    rootfs: RustMount,
    seed: RustPodContainerConfiguration,
    configuration: RustConfigurePodContainer
  ) -> CzOutcome {
    let pod = pod
    let id = id.toString()
    let rootfs = Containerization.Mount(rootfs)
    // Rust's: the pod calls the closure once, while `blocking` waits.
    nonisolated(unsafe) let seed = seed
    nonisolated(unsafe) let configuration = configuration

    return CzOutcome {
      try blocking {
        try await pod.addContainer(id, rootfs: rootfs) { config in
          configuration.call(seed)
          config = try LinuxPod.ContainerConfiguration(seed)
        }
      }
    }
  }

  func create() -> CzOutcome {
    let pod = pod

    return CzOutcome { try blocking { try await pod.create() } }
  }

  func startContainer(id: RustStr) -> CzOutcome {
    let pod = pod
    let id = id.toString()

    return CzOutcome { try blocking { try await pod.startContainer(id) } }
  }

  func stopContainer(id: RustStr) -> CzOutcome {
    let pod = pod
    let id = id.toString()

    return CzOutcome { try blocking { try await pod.stopContainer(id) } }
  }

  func stop() -> CzOutcome {
    let pod = pod

    return CzOutcome { try blocking { try await pod.stop() } }
  }

  func killContainer(id: RustStr, signal: Int32) -> CzOutcome {
    let pod = pod
    let id = id.toString()

    return CzOutcome { try blocking { try await pod.killContainer(id, signal: Signal(rawValue: signal)) } }
  }

  func waitContainer(id: RustStr, timeoutInSeconds: Int64?) -> CzOutcome {
    let pod = pod
    let id = id.toString()

    return CzOutcome { try blocking { try await pod.waitContainer(id, timeoutInSeconds: timeoutInSeconds) } }
  }

  func resizeContainer(id: RustStr, width: UInt16, height: UInt16) -> CzOutcome {
    let pod = pod
    let id = id.toString()

    return CzOutcome {
      try blocking { try await pod.resizeContainer(id, to: Terminal.Size(width: width, height: height)) }
    }
  }

  /// `seed` and `configuration` as in a container's `execWith`.
  func execInContainer(
    id: RustStr,
    processID: RustStr,
    seed: RustLinuxProcessConfiguration,
    configuration: RustConfigureProcess
  ) -> CzOutcome {
    let pod = pod
    let id = id.toString()
    let processID = processID.toString()
    let configure = configured(seed, by: configuration)

    return CzOutcome {
      CzLinuxProcess(
        try blocking { try await pod.execInContainer(id, processID: processID, configuration: configure) }
      )
    }
  }

  /// Swift's doesn't throw, so nor does `blocking` here.
  func listContainers() -> RustVec<RustString> {
    let pod = pod

    return rustStrings((try? blocking { await pod.listContainers() }) ?? [])
  }

  func statistics(hasContainerIDs: Bool, containerIDs: RustVec<RustString>, categories: Int64) -> CzOutcome {
    let pod = pod
    let containerIDs = hasContainerIDs ? strings(containerIDs) : nil
    let categories = StatCategory(rawValue: Int(categories))

    return CzOutcome {
      try blocking { try await pod.statistics(containerIDs: containerIDs, categories: categories) }
    }
  }

  /// The dialed handle doesn't close its descriptor, so Rust takes it.
  func dialVsock(port: UInt32) -> CzOutcome {
    let pod = pod

    return CzOutcome { try blocking { try await pod.dialVsock(port: port) }.fileDescriptor }
  }

  /// What `withVirtualMachineInstance` hands its closure, for Rust's to use.
  func virtualMachineInstance() -> CzOutcome {
    let pod = pod

    return CzOutcome {
      CzVirtualMachineInstance(try blocking { try await pod.withVirtualMachineInstance { $0 } })
    }
  }

  func filesystemOperation(id: RustStr, operation: FilesystemOperationKind, path: RustStr) -> CzOutcome {
    let pod = pod
    let id = id.toString()
    let path = path.toString()
    let operation: FilesystemOperation =
      switch operation {
      case .Freeze: .freeze
      case .Thaw: .thaw
      case .Trim: .trim
      }

    return CzOutcome { try blocking { try await pod.filesystemOperation(id, operation: operation, path: path) } }
  }

  func closeContainerStdin(id: RustStr) -> CzOutcome {
    let pod = pod
    let id = id.toString()

    return CzOutcome { try blocking { try await pod.closeContainerStdin(id) } }
  }

  func relayUnixSocket(id: RustStr, socket: RustUnixSocketConfiguration) -> CzOutcome {
    let pod = pod
    let id = id.toString()

    return CzOutcome {
      let socket = try UnixSocketConfiguration(socket)

      return try blocking { try await pod.relayUnixSocket(id, socket: socket) }
    }
  }
}