containerization-framework 0.4.0

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

import Containerization
import ContainerizationOS
import Foundation

final class CzLinuxContainer: Sendable {
  let container: LinuxContainer

  init(_ container: LinuxContainer) {
    self.container = container
  }

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

  func rootfs() -> CzOutcome {
    CzOutcome.holding(container.rootfs)
  }

  func writableLayer() -> CzOutcome {
    CzOutcome.holding(container.writableLayer)
  }

  func config(seed: RustLinuxContainerConfiguration, receive: RustConfigure) {
    fill(seed, from: container.config)
    receive.call(seed)
  }

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

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

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

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

  func start() -> CzOutcome {
    let container = container

    return CzOutcome { try blocking { try await container.start() } }
  }

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

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

  func kill(signal: Int32) -> CzOutcome {
    let container = container

    return CzOutcome { try blocking { try await container.kill(Signal(rawValue: signal)) } }
  }

  func wait(timeoutInSeconds: Int64?) -> CzOutcome {
    let container = container

    return CzOutcome { try blocking { try await container.wait(timeoutInSeconds: timeoutInSeconds) } }
  }

  func resize(width: UInt16, height: UInt16) -> CzOutcome {
    let container = container

    return CzOutcome { try blocking { try await container.resize(to: Terminal.Size(width: width, height: height)) } }
  }

  func exec(id: RustStr, configuration: RustLinuxProcessConfiguration) -> CzOutcome {
    let id = id.toString()
    let container = container

    return CzOutcome {
      let configuration = try LinuxProcessConfiguration(configuration)

      return CzLinuxProcess(try blocking { try await container.exec(id, configuration: configuration) })
    }
  }

  /// `configuration` is Rust's closure, and `seed` Rust's to fill, as in the
  /// manager's `create`.
  func execWith(id: RustStr, seed: RustLinuxProcessConfiguration, configuration: RustConfigureProcess) -> CzOutcome {
    let id = id.toString()
    let container = container
    let configure = configured(seed, by: configuration)

    return CzOutcome {
      CzLinuxProcess(try blocking { try await container.exec(id, configuration: configure) })
    }
  }

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

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

  func closeStdin() -> CzOutcome {
    let container = container

    return CzOutcome { try blocking { try await container.closeStdin() } }
  }

  func statistics(categories: Int64) -> CzOutcome {
    let container = container
    let categories = StatCategory(rawValue: Int(categories))

    return CzOutcome { try blocking { try await container.statistics(categories: categories) } }
  }

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

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

  func copyIn(source: RustStr, destination: RustStr, mode: UInt32, createParents: Bool, chunkSize: UInt) -> CzOutcome {
    let container = container
    let source = URL(filePath: source.toString())
    let destination = URL(filePath: destination.toString())

    return CzOutcome {
      try blocking {
        try await container.copyIn(
          from: source,
          to: destination,
          mode: mode,
          createParents: createParents,
          chunkSize: Int(chunkSize)
        )
      }
    }
  }

  func copyOut(source: RustStr, destination: RustStr, createParents: Bool, chunkSize: UInt) -> CzOutcome {
    let container = container
    let source = URL(filePath: source.toString())
    let destination = URL(filePath: destination.toString())

    return CzOutcome {
      try blocking {
        try await container.copyOut(
          from: source,
          to: destination,
          createParents: createParents,
          chunkSize: Int(chunkSize)
        )
      }
    }
  }
}

/// The closure an `exec` takes: fills `seed` with the configuration Swift
/// seeded, lets Rust's closure change it, and takes it back.
func configured(
  _ seed: RustLinuxProcessConfiguration,
  by configuration: RustConfigureProcess
) -> @Sendable (inout LinuxProcessConfiguration) throws -> Void {
  // Rust's: an `exec` calls this once, while `blocking` waits.
  nonisolated(unsafe) let seed = seed
  nonisolated(unsafe) let configuration = configuration

  return { config in
    fill(seed, from: config)
    configuration.call(seed)
    config = try LinuxProcessConfiguration(seed)
  }
}

// MARK: For unit tests

func linuxContainerDefaultMounts() -> CzOutcome {
  CzOutcome { [LinuxContainer.defaultMounts(), LinuxContainer.defaultOCIMounts()] }
}

func linuxContainerDefaultCopyChunkSize() -> UInt {
  UInt(LinuxContainer.defaultCopyChunkSize)
}

// MARK: ContainerStatistics

extension CzOutcome {
  /// The `id`, `interface` or `mountPoint` of the statistics held.
  func statisticsName() -> String {
    switch held(Any.self) {
    case let statistics as ContainerStatistics: statistics.id
    case let network as ContainerStatistics.NetworkStatistics: network.interface
    case let filesystem as ContainerStatistics.FilesystemStatistics: filesystem.mountPoint
    case let other: cast(other)
    }
  }

  /// The `UInt64` fields of the statistics held, in the order Swift declares
  /// them.
  func statisticsNumbers() -> RustVec<UInt64> {
    let numbers: [UInt64] =
      switch held(Any.self) {
      case let process as ContainerStatistics.ProcessStatistics:
        [process.current, process.limit]
      case let memory as ContainerStatistics.MemoryStatistics:
        [
          memory.usageBytes, memory.limitBytes, memory.swapUsageBytes, memory.swapLimitBytes, memory.cacheBytes,
          memory.kernelStackBytes, memory.slabBytes, memory.pageFaults, memory.majorPageFaults,
          memory.inactiveFile, memory.anon, memory.workingsetRefaultAnon, memory.workingsetRefaultFile,
          memory.pgstealKswapd, memory.pgstealDirect, memory.pgstealKhugepaged,
        ]
      case let cpu as ContainerStatistics.CPUStatistics:
        [
          cpu.usageUsec, cpu.userUsec, cpu.systemUsec, cpu.throttlingPeriods, cpu.throttledPeriods,
          cpu.throttledTimeUsec,
        ]
      case let device as ContainerStatistics.BlockIODevice:
        [
          device.major, device.minor, device.readBytes, device.writeBytes, device.readOperations,
          device.writeOperations,
        ]
      case let network as ContainerStatistics.NetworkStatistics:
        [
          network.receivedPackets, network.transmittedPackets, network.receivedBytes, network.transmittedBytes,
          network.receivedErrors, network.transmittedErrors,
        ]
      case let events as ContainerStatistics.MemoryEventStatistics:
        [events.low, events.high, events.max, events.oom, events.oomKill]
      case let filesystem as ContainerStatistics.FilesystemStatistics:
        [filesystem.blockSize, filesystem.blocks, filesystem.freeBlocks, filesystem.inodes, filesystem.freeInodes]
      case let other: cast(other)
      }

    let vec = RustVec<UInt64>()
    for number in numbers {
      vec.push(value: number)
    }
    return vec
  }

  /// The part of the held `ContainerStatistics` that `category` reports.
  func statisticsCategory(category: Int64) -> CzOutcome {
    let statistics: ContainerStatistics = taken()

    return switch StatCategory(rawValue: Int(category)) {
    case .process: CzOutcome.holding(statistics.process)
    case .memory: CzOutcome.holding(statistics.memory)
    case .cpu: CzOutcome.holding(statistics.cpu)
    case .blockIO: CzOutcome.holding(statistics.blockIO?.devices)
    case .network: CzOutcome.holding(statistics.networks)
    case .memoryEvents: CzOutcome.holding(statistics.memoryEvents)
    case .filesystem: CzOutcome.holding(statistics.filesystem)
    default: preconditionFailure("Rust asks for one category at a time, not \(category)")
    }
  }
}

final class CzLinuxProcess: Sendable {
  let process: LinuxProcess

  init(_ process: LinuxProcess) {
    self.process = process
  }

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

  func owningContainer() -> String? {
    process.owningContainer
  }

  func pid() -> Int32 {
    process.pid
  }

  func start() -> CzOutcome {
    let process = process

    return CzOutcome { try blocking { try await process.start() } }
  }

  func kill(signal: Int32) -> CzOutcome {
    let process = process

    return CzOutcome { try blocking { try await process.kill(Signal(rawValue: signal)) } }
  }

  func resize(width: UInt16, height: UInt16) -> CzOutcome {
    let process = process

    return CzOutcome { try blocking { try await process.resize(to: Terminal.Size(width: width, height: height)) } }
  }

  func closeStdin() -> CzOutcome {
    let process = process

    return CzOutcome { try blocking { try await process.closeStdin() } }
  }

  func wait(timeoutInSeconds: Int64?) -> CzOutcome {
    let process = process

    return CzOutcome { try blocking { try await process.wait(timeoutInSeconds: timeoutInSeconds) } }
  }

  func delete() -> CzOutcome {
    let process = process

    return CzOutcome { try blocking { try await process.delete() } }
  }
}