mirror of
https://github.com/apple/container.git
synced 2026-09-11 18:25:41 +00:00
We should be able to supply an ImageStore if the user wants a different ContentStore, or a different path.
485 lines
17 KiB
Swift
485 lines
17 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#if os(macOS)
|
|
|
|
import ContainerizationError
|
|
import ContainerizationOCI
|
|
import ContainerizationOS
|
|
import Foundation
|
|
import ContainerizationExtras
|
|
import Virtualization
|
|
import vmnet
|
|
|
|
/// A manager for creating and running containers.
|
|
/// Supports container networking options.
|
|
public struct ContainerManager: Sendable {
|
|
public let imageStore: ImageStore
|
|
private let vmm: VirtualMachineManager
|
|
private let network: Network?
|
|
|
|
private var containerRoot: URL {
|
|
self.imageStore.path.appendingPathComponent("containers")
|
|
}
|
|
|
|
/// A network that can allocate and release interfaces for use with containers.
|
|
public protocol Network: Sendable {
|
|
func create(_ id: String) throws -> Interface?
|
|
func release(_ id: String) throws
|
|
}
|
|
|
|
/// A network backed by vmnet on macOS.
|
|
@available(macOS 26.0, *)
|
|
public struct VmnetNetwork: Network {
|
|
private let allocator: Allocator
|
|
nonisolated(unsafe) private let reference: vmnet_network_ref
|
|
|
|
/// The IPv4 subnet of this network.
|
|
public let subnet: CIDRAddress
|
|
|
|
/// The gateway address of this network.
|
|
public var gateway: IPv4Address {
|
|
subnet.gateway
|
|
}
|
|
|
|
struct Allocator: Sendable {
|
|
private let addressAllocator: any AddressAllocator<UInt32>
|
|
private let cidr: CIDRAddress
|
|
private var allocations: [String: UInt32]
|
|
|
|
init(cidr: CIDRAddress) throws {
|
|
self.cidr = cidr
|
|
self.allocations = .init()
|
|
let size = Int(cidr.upper.value - cidr.lower.value - 3)
|
|
self.addressAllocator = try UInt32.rotatingAllocator(
|
|
lower: cidr.lower.value + 2,
|
|
size: UInt32(size)
|
|
)
|
|
}
|
|
|
|
func allocate(_ id: String) throws -> String {
|
|
if allocations[id] != nil {
|
|
throw ContainerizationError(.exists, message: "allocation with id \(id) already exists")
|
|
}
|
|
let index = try addressAllocator.allocate()
|
|
let ip = IPv4Address(fromValue: index)
|
|
return try CIDRAddress(ip, prefixLength: cidr.prefixLength).description
|
|
}
|
|
|
|
func release(_ id: String) throws {
|
|
if let index = self.allocations[id] {
|
|
try addressAllocator.release(index)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A network interface supporting the vmnet_network_ref.
|
|
public struct Interface: Containerization.Interface, VZInterface, Sendable {
|
|
public let address: String
|
|
public let gateway: String?
|
|
public let macAddress: String?
|
|
|
|
nonisolated(unsafe) private let reference: vmnet_network_ref
|
|
|
|
public init(
|
|
reference: vmnet_network_ref,
|
|
address: String,
|
|
gateway: String,
|
|
macAddress: String? = nil
|
|
) {
|
|
self.address = address
|
|
self.gateway = gateway
|
|
self.macAddress = macAddress
|
|
self.reference = reference
|
|
}
|
|
|
|
/// Returns the underlying `VZVirtioNetworkDeviceConfiguration`.
|
|
public func device() throws -> VZVirtioNetworkDeviceConfiguration {
|
|
let config = VZVirtioNetworkDeviceConfiguration()
|
|
if let macAddress = self.macAddress {
|
|
guard let mac = VZMACAddress(string: macAddress) else {
|
|
throw ContainerizationError(.invalidArgument, message: "invalid mac address \(macAddress)")
|
|
}
|
|
config.macAddress = mac
|
|
}
|
|
config.attachment = VZVmnetNetworkDeviceAttachment(network: self.reference)
|
|
return config
|
|
}
|
|
}
|
|
|
|
/// Creates a new network.
|
|
/// - Parameter subnet: The subnet to use for this network.
|
|
public init(subnet: String? = nil) throws {
|
|
var status: vmnet_return_t = .VMNET_FAILURE
|
|
guard let config = vmnet_network_configuration_create(.VMNET_SHARED_MODE, &status) else {
|
|
throw ContainerizationError(.unsupported, message: "failed to create vmnet config with status \(status)")
|
|
}
|
|
|
|
vmnet_network_configuration_disable_dhcp(config)
|
|
|
|
if let subnet {
|
|
try Self.configureSubnet(config, subnet: try CIDRAddress(subnet))
|
|
}
|
|
|
|
guard let ref = vmnet_network_create(config, &status), status == .VMNET_SUCCESS else {
|
|
throw ContainerizationError(.unsupported, message: "failed to create vmnet network with status \(status)")
|
|
}
|
|
|
|
let cidr = try Self.getSubnet(ref)
|
|
|
|
self.allocator = try .init(cidr: cidr)
|
|
self.subnet = cidr
|
|
self.reference = ref
|
|
}
|
|
|
|
/// Returns a new interface for use with a container.
|
|
/// - Parameter id: The container ID.
|
|
public func create(_ id: String) throws -> Containerization.Interface? {
|
|
let address = try allocator.allocate(id)
|
|
return Self.Interface(
|
|
reference: self.reference,
|
|
address: address,
|
|
gateway: self.gateway.description,
|
|
)
|
|
}
|
|
|
|
/// Performs cleanup of an interface.
|
|
/// - Parameter id: The container ID.
|
|
public func release(_ id: String) throws {
|
|
try allocator.release(id)
|
|
}
|
|
|
|
private static func getSubnet(_ ref: vmnet_network_ref) throws -> CIDRAddress {
|
|
var subnet = in_addr()
|
|
var mask = in_addr()
|
|
vmnet_network_get_ipv4_subnet(ref, &subnet, &mask)
|
|
|
|
let sa = UInt32(bigEndian: subnet.s_addr)
|
|
let mv = UInt32(bigEndian: mask.s_addr)
|
|
|
|
let lower = IPv4Address(fromValue: sa & mv)
|
|
let upper = IPv4Address(fromValue: lower.value + ~mv)
|
|
|
|
return try CIDRAddress(lower: lower, upper: upper)
|
|
}
|
|
|
|
private static func configureSubnet(_ config: vmnet_network_configuration_ref, subnet: CIDRAddress) throws {
|
|
let gateway = subnet.gateway
|
|
|
|
var ga = in_addr()
|
|
inet_pton(AF_INET, gateway.description, &ga)
|
|
|
|
let mask = IPv4Address(fromValue: subnet.prefixLength.prefixMask32)
|
|
var ma = in_addr()
|
|
inet_pton(AF_INET, mask.description, &ma)
|
|
|
|
guard vmnet_network_configuration_set_ipv4_subnet(config, &ga, &ma) == .VMNET_SUCCESS else {
|
|
throw ContainerizationError(.internalError, message: "failed to set subnet \(subnet) for network")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a new manager with the provided kernel, initfs mount, image store
|
|
/// and optional network implementation.
|
|
public init(
|
|
kernel: Kernel,
|
|
initfs: Mount,
|
|
imageStore: ImageStore,
|
|
network: Network? = nil
|
|
) throws {
|
|
self.imageStore = imageStore
|
|
self.network = network
|
|
try Self.createRootDirectory(path: self.imageStore.path)
|
|
self.vmm = VZVirtualMachineManager(
|
|
kernel: kernel,
|
|
initialFilesystem: initfs,
|
|
bootlog: self.imageStore.path.appendingPathComponent("bootlog.log").absolutePath()
|
|
)
|
|
}
|
|
|
|
/// Create a new manager with the provided kernel, initfs mount, root state
|
|
/// directory and optional network implementation.
|
|
public init(
|
|
kernel: Kernel,
|
|
initfs: Mount,
|
|
root: URL? = nil,
|
|
network: Network? = nil
|
|
) throws {
|
|
if let root {
|
|
self.imageStore = try ImageStore(path: root)
|
|
} else {
|
|
self.imageStore = ImageStore.default
|
|
}
|
|
self.network = network
|
|
try Self.createRootDirectory(path: self.imageStore.path)
|
|
self.vmm = VZVirtualMachineManager(
|
|
kernel: kernel,
|
|
initialFilesystem: initfs,
|
|
bootlog: self.imageStore.path.appendingPathComponent("bootlog.log").absolutePath()
|
|
)
|
|
}
|
|
|
|
/// Create a new manager with the provided kernel, initfs reference, image store
|
|
/// and optional network implementation.
|
|
public init(
|
|
kernel: Kernel,
|
|
initfsReference: String,
|
|
imageStore: ImageStore,
|
|
network: Network? = nil
|
|
) async throws {
|
|
self.imageStore = imageStore
|
|
self.network = network
|
|
try Self.createRootDirectory(path: self.imageStore.path)
|
|
|
|
let initPath = self.imageStore.path.appendingPathComponent("initfs.ext4")
|
|
let initImage = try await self.imageStore.getInitImage(reference: initfsReference)
|
|
let initfs = try await {
|
|
do {
|
|
return try await initImage.initBlock(at: initPath, for: .linuxArm)
|
|
} catch let err as ContainerizationError {
|
|
guard err.code == .exists else {
|
|
throw err
|
|
}
|
|
return .block(
|
|
format: "ext4",
|
|
source: initPath.absolutePath(),
|
|
destination: "/",
|
|
options: ["ro"]
|
|
)
|
|
}
|
|
}()
|
|
|
|
self.vmm = VZVirtualMachineManager(
|
|
kernel: kernel,
|
|
initialFilesystem: initfs,
|
|
bootlog: self.imageStore.path.appendingPathComponent("bootlog.log").absolutePath()
|
|
)
|
|
}
|
|
|
|
/// Create a new manager with the provided kernel and image reference for the initfs.
|
|
public init(
|
|
kernel: Kernel,
|
|
initfsReference: String,
|
|
root: URL? = nil,
|
|
network: Network? = nil
|
|
) async throws {
|
|
if let root {
|
|
self.imageStore = try ImageStore(path: root)
|
|
} else {
|
|
self.imageStore = ImageStore.default
|
|
}
|
|
self.network = network
|
|
try Self.createRootDirectory(path: self.imageStore.path)
|
|
|
|
let initPath = self.imageStore.path.appendingPathComponent("initfs.ext4")
|
|
let initImage = try await self.imageStore.getInitImage(reference: initfsReference)
|
|
let initfs = try await {
|
|
do {
|
|
return try await initImage.initBlock(at: initPath, for: .linuxArm)
|
|
} catch let err as ContainerizationError {
|
|
guard err.code == .exists else {
|
|
throw err
|
|
}
|
|
return .block(
|
|
format: "ext4",
|
|
source: initPath.absolutePath(),
|
|
destination: "/",
|
|
options: ["ro"]
|
|
)
|
|
}
|
|
}()
|
|
|
|
self.vmm = VZVirtualMachineManager(
|
|
kernel: kernel,
|
|
initialFilesystem: initfs,
|
|
bootlog: self.imageStore.path.appendingPathComponent("bootlog.log").absolutePath()
|
|
)
|
|
}
|
|
|
|
/// Create a new manager with the provided vmm and network.
|
|
public init(
|
|
vmm: any VirtualMachineManager,
|
|
network: Network? = nil
|
|
) throws {
|
|
self.imageStore = ImageStore.default
|
|
try Self.createRootDirectory(path: self.imageStore.path)
|
|
self.network = network
|
|
self.vmm = vmm
|
|
}
|
|
|
|
private static func createRootDirectory(path: URL) throws {
|
|
try FileManager.default.createDirectory(
|
|
at: path.appendingPathComponent("containers"),
|
|
withIntermediateDirectories: true
|
|
)
|
|
}
|
|
|
|
/// Returns a new container from the provided image reference.
|
|
/// - Parameters:
|
|
/// - id: The container ID.
|
|
/// - reference: The image reference.
|
|
/// - rootfsSizeInBytes: The size of the root filesystem in bytes. Defaults to 8 GiB.
|
|
public func create(
|
|
_ id: String,
|
|
reference: String,
|
|
rootfsSizeInBytes: UInt64 = 8.gib(),
|
|
configuration: (inout LinuxContainer.Configuration) throws -> Void
|
|
) async throws -> LinuxContainer {
|
|
let image = try await imageStore.get(reference: reference, pull: true)
|
|
return try await create(
|
|
id,
|
|
image: image,
|
|
rootfsSizeInBytes: rootfsSizeInBytes,
|
|
configuration: configuration
|
|
)
|
|
}
|
|
|
|
/// Returns a new container from the provided image.
|
|
/// - Parameters:
|
|
/// - id: The container ID.
|
|
/// - image: The image.
|
|
/// - rootfsSizeInBytes: The size of the root filesystem in bytes. Defaults to 8 GiB.
|
|
public func create(
|
|
_ id: String,
|
|
image: Image,
|
|
rootfsSizeInBytes: UInt64 = 8.gib(),
|
|
configuration: (inout LinuxContainer.Configuration) throws -> Void
|
|
) async throws -> LinuxContainer {
|
|
let path = try createContainerRoot(id)
|
|
|
|
let rootfs = try await unpack(
|
|
image: image,
|
|
destination: path.appendingPathComponent("rootfs.ext4"),
|
|
size: rootfsSizeInBytes
|
|
)
|
|
return try await create(
|
|
id,
|
|
image: image,
|
|
rootfs: rootfs,
|
|
configuration: configuration
|
|
)
|
|
}
|
|
|
|
/// Returns a new container from the provided image and root filesystem mount.
|
|
/// - Parameters:
|
|
/// - id: The container ID.
|
|
/// - image: The image.
|
|
/// - rootfs: The root filesystem mount pointing to an existing block file.
|
|
public func create(
|
|
_ id: String,
|
|
image: Image,
|
|
rootfs: Mount,
|
|
configuration: (inout LinuxContainer.Configuration) throws -> Void
|
|
) async throws -> LinuxContainer {
|
|
let imageConfig = try await image.config(for: .current).config
|
|
return try LinuxContainer(
|
|
id,
|
|
rootfs: rootfs,
|
|
vmm: self.vmm
|
|
) { config in
|
|
if let imageConfig {
|
|
config.process = .init(from: imageConfig)
|
|
}
|
|
if let interface = try self.network?.create(id) {
|
|
config.interfaces = [interface]
|
|
config.dns = .init(nameservers: [interface.gateway!])
|
|
}
|
|
try configuration(&config)
|
|
}
|
|
}
|
|
|
|
/// Returns an existing container from the provided image and root filesystem mount.
|
|
/// - Parameters:
|
|
/// - id: The container ID.
|
|
/// - image: The image.
|
|
public func get(
|
|
_ id: String,
|
|
image: Image,
|
|
) async throws -> LinuxContainer {
|
|
let path = containerRoot.appendingPathComponent(id)
|
|
guard FileManager.default.fileExists(atPath: path.absolutePath()) else {
|
|
throw ContainerizationError(.notFound, message: "\(id) does not exist")
|
|
}
|
|
|
|
let rootfs: Mount = .block(
|
|
format: "ext4",
|
|
source: path.appendingPathComponent("rootfs.ext4").absolutePath(),
|
|
destination: "/",
|
|
options: []
|
|
)
|
|
|
|
let imageConfig = try await image.config(for: .current).config
|
|
return try LinuxContainer(
|
|
id,
|
|
rootfs: rootfs,
|
|
vmm: self.vmm
|
|
) { config in
|
|
if let imageConfig {
|
|
config.process = .init(from: imageConfig)
|
|
}
|
|
if let interface = try self.network?.create(id) {
|
|
config.interfaces = [interface]
|
|
config.dns = .init(nameservers: [interface.gateway!])
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Performs the cleanup of a container.
|
|
/// - Parameter id: The container ID.
|
|
public func delete(_ id: String) throws {
|
|
try self.network?.release(id)
|
|
let path = containerRoot.appendingPathComponent(id)
|
|
try FileManager.default.removeItem(at: path)
|
|
}
|
|
|
|
private func createContainerRoot(_ id: String) throws -> URL {
|
|
let path = containerRoot.appendingPathComponent(id)
|
|
try FileManager.default.createDirectory(at: path, withIntermediateDirectories: false)
|
|
return path
|
|
}
|
|
|
|
private func unpack(image: Image, destination: URL, size: UInt64) async throws -> Mount {
|
|
do {
|
|
let unpacker = EXT4Unpacker(blockSizeInBytes: size)
|
|
return try await unpacker.unpack(image, for: .current, at: destination)
|
|
} catch let err as ContainerizationError {
|
|
if err.code == .exists {
|
|
return .block(
|
|
format: "ext4",
|
|
source: destination.absolutePath(),
|
|
destination: "/",
|
|
options: []
|
|
)
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
extension CIDRAddress {
|
|
/// The gateway address of the network.
|
|
public var gateway: IPv4Address {
|
|
IPv4Address(fromValue: self.lower.value + 1)
|
|
}
|
|
}
|
|
|
|
@available(macOS 26.0, *)
|
|
private struct SendableReference: Sendable {
|
|
nonisolated(unsafe) private let reference: vmnet_network_ref
|
|
}
|
|
|
|
#endif
|