chore: import upstream snapshot with attribution
container project - merge build / Invoke build (push) Successful in 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:06:18 +08:00
commit e84fb4a79e
474 changed files with 68321 additions and 0 deletions
@@ -0,0 +1,174 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import Containerization
import ContainerizationError
import Foundation
public struct Bundle: Sendable {
private static let initfsFilename = "initfs.ext4"
private static let kernelFilename = "kernel.json"
private static let kernelBinaryFilename = "kernel.bin"
private static let containerRootFsBlockFilename = "rootfs.ext4"
private static let containerRootFsFilename = "rootfs.json"
static let containerConfigFilename = "config.json"
/// The path to the bundle.
public let path: URL
public init(path: URL) {
self.path = path
}
public var bootlog: URL {
self.path.appendingPathComponent("vminitd.log")
}
public var containerRootfsBlock: URL {
self.path.appendingPathComponent(Self.containerRootFsBlockFilename)
}
private var containerRootfsConfig: URL {
self.path.appendingPathComponent(Self.containerRootFsFilename)
}
public var containerRootfs: Filesystem {
get throws {
let data = try Data(contentsOf: containerRootfsConfig)
let fs = try JSONDecoder().decode(Filesystem.self, from: data)
return fs
}
}
/// Return the initial filesystem for a sandbox.
public var initialFilesystem: Filesystem {
.block(
format: "ext4",
source: self.path.appendingPathComponent(Self.initfsFilename).path,
destination: "/",
options: ["ro"]
)
}
public var kernel: Kernel {
get throws {
try load(path: self.path.appendingPathComponent(Self.kernelFilename))
}
}
public var configuration: ContainerConfiguration {
get throws {
try load(path: self.path.appendingPathComponent(Self.containerConfigFilename))
}
}
}
extension Bundle {
public static func create(
path: URL,
initialFilesystem: Filesystem,
kernel: Kernel,
containerConfiguration: ContainerConfiguration? = nil,
containerRootFilesystem: Filesystem? = nil,
options: ContainerCreateOptions? = nil
) throws -> Bundle {
try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true)
let kbin = path.appendingPathComponent(Self.kernelBinaryFilename)
try FileManager.default.copyItem(at: kernel.path, to: kbin)
var k = kernel
k.path = kbin
try write(path.appendingPathComponent(Self.kernelFilename), value: k)
switch initialFilesystem.type {
case .block(let fmt, _, _):
guard fmt == "ext4" else {
fatalError("ext4 is the only supported format for initial filesystem")
}
// when saving the Initial Filesystem to the bundle
// discard any filesystem information and just persist
// the block into the Bundle.
_ = try initialFilesystem.clone(to: path.appendingPathComponent(Self.initfsFilename).path)
default:
fatalError("invalid filesystem type for initial filesystem")
}
let bundle = Bundle(path: path)
if let containerConfiguration {
try bundle.write(filename: Self.containerConfigFilename, value: containerConfiguration)
}
if let rootFsOverride = options?.rootFsOverride {
try bundle.setContainerRootFs(fs: rootFsOverride)
} else if let containerRootFilesystem {
let readonly = containerConfiguration?.readOnly ?? false
try bundle.cloneContainerRootFs(cloning: containerRootFilesystem, readonly: readonly)
}
if let options {
try bundle.write(filename: "options.json", value: options)
}
return bundle
}
}
extension Bundle {
/// Set the value of the configuration for the Bundle.
public func set(configuration: ContainerConfiguration) throws {
try write(filename: Self.containerConfigFilename, value: configuration)
}
/// Return the full filepath for a named resource in the Bundle.
public func filePath(for name: String) -> URL {
path.appendingPathComponent(name)
}
public func setContainerRootFs(fs: Filesystem) throws {
let fsData = try JSONEncoder().encode(fs)
try fsData.write(to: self.containerRootfsConfig)
}
public func cloneContainerRootFs(cloning fs: Filesystem, readonly: Bool = false) throws {
var mutableFs = fs
if readonly && !mutableFs.options.contains("ro") {
mutableFs.options.append("ro")
}
let cloned = try mutableFs.clone(to: self.containerRootfsBlock.absolutePath())
try setContainerRootFs(fs: cloned)
}
/// Delete the bundle and all of the resources contained inside.
public func delete() throws {
try FileManager.default.removeItem(at: self.path)
}
public func write(filename: String, value: Encodable) throws {
try Self.write(self.path.appendingPathComponent(filename), value: value)
}
private static func write(_ path: URL, value: Encodable) throws {
let data = try JSONEncoder().encode(value)
try data.write(to: path)
}
public func load<T>(filename: String) throws -> T where T: Decodable {
try load(path: self.path.appendingPathComponent(filename))
}
private func load<T>(path: URL) throws -> T where T: Decodable {
let data = try Data(contentsOf: path)
return try JSONDecoder().decode(T.self, from: data)
}
}
@@ -0,0 +1,182 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import ContainerizationOCI
import Foundation
public struct ContainerConfiguration: Sendable, Codable {
/// Identifier for the container.
public var id: String
/// Image used to create the container.
public var image: ImageDescription
/// External mounts to add to the container.
public var mounts: [Filesystem] = []
/// Ports to publish from container to host.
public var publishedPorts: [PublishPort] = []
/// Sockets to publish from container to host.
public var publishedSockets: [PublishSocket] = []
/// Key/Value labels for the container.
public var labels: [String: String] = [:]
/// System controls for the container.
public var sysctls: [String: String] = [:]
/// The networks the container will be added to.
public var networks: [AttachmentConfiguration] = []
/// The DNS configuration for the container.
public var dns: DNSConfiguration? = nil
/// Whether to enable rosetta x86-64 translation for the container.
public var rosetta: Bool = false
/// Initial or main process of the container.
public var initProcess: ProcessConfiguration
/// Platform for the container.
public var platform: ContainerizationOCI.Platform = .current
/// Resource values for the container.
public var resources: Resources = .init()
/// Name of the runtime that supports the container.
public var runtimeHandler: String = "container-runtime-linux"
/// Configure exposing virtualization support in the container.
public var virtualization: Bool = false
/// Enable SSH agent socket forwarding from host to container.
public var ssh: Bool = false
/// Whether to mount the rootfs as read-only.
public var readOnly: Bool = false
/// Whether to use a minimal init process inside the container.
public var useInit: Bool = false
/// Linux capabilities to add (normalized CAP_* strings, or "ALL").
public var capAdd: [String] = []
/// Linux capabilities to drop (normalized CAP_* strings, or "ALL").
public var capDrop: [String] = []
/// Size of /dev/shm in bytes. When nil, the default size is used.
public var shmSize: UInt64?
/// Signal to send to the container process on stop (from image config).
public var stopSignal: String?
/// The time at which the container was created.
public var creationDate: Date = Date()
enum CodingKeys: String, CodingKey {
case id
case image
case mounts
case publishedPorts
case publishedSockets
case labels
case sysctls
case networks
case dns
case rosetta
case initProcess
case platform
case resources
case runtimeHandler
case virtualization
case ssh
case readOnly
case useInit
case capAdd
case capDrop
case shmSize
case stopSignal
case creationDate
}
/// Create a configuration from the supplied Decoder, initializing missing
/// values where possible to reasonable defaults.
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
image = try container.decode(ImageDescription.self, forKey: .image)
mounts = try container.decodeIfPresent([Filesystem].self, forKey: .mounts) ?? []
publishedPorts = try container.decodeIfPresent([PublishPort].self, forKey: .publishedPorts) ?? []
publishedSockets = try container.decodeIfPresent([PublishSocket].self, forKey: .publishedSockets) ?? []
labels = try container.decodeIfPresent([String: String].self, forKey: .labels) ?? [:]
sysctls = try container.decodeIfPresent([String: String].self, forKey: .sysctls) ?? [:]
if container.contains(.networks) {
networks = try container.decode([AttachmentConfiguration].self, forKey: .networks)
} else {
networks = []
}
dns = try container.decodeIfPresent(DNSConfiguration.self, forKey: .dns)
rosetta = try container.decodeIfPresent(Bool.self, forKey: .rosetta) ?? false
initProcess = try container.decode(ProcessConfiguration.self, forKey: .initProcess)
platform = try container.decodeIfPresent(ContainerizationOCI.Platform.self, forKey: .platform) ?? .current
resources = try container.decodeIfPresent(Resources.self, forKey: .resources) ?? .init()
runtimeHandler = try container.decodeIfPresent(String.self, forKey: .runtimeHandler) ?? "container-runtime-linux"
virtualization = try container.decodeIfPresent(Bool.self, forKey: .virtualization) ?? false
ssh = try container.decodeIfPresent(Bool.self, forKey: .ssh) ?? false
readOnly = try container.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
useInit = try container.decodeIfPresent(Bool.self, forKey: .useInit) ?? false
capAdd = try container.decodeIfPresent([String].self, forKey: .capAdd) ?? []
capDrop = try container.decodeIfPresent([String].self, forKey: .capDrop) ?? []
shmSize = try container.decodeIfPresent(UInt64.self, forKey: .shmSize)
stopSignal = try container.decodeIfPresent(String.self, forKey: .stopSignal)
creationDate = try container.decodeIfPresent(Date.self, forKey: .creationDate) ?? Date(timeIntervalSince1970: 0)
}
public struct DNSConfiguration: Sendable, Codable {
public static let defaultNameservers = ["1.1.1.1"]
public let nameservers: [String]
public let domain: String?
public let searchDomains: [String]
public let options: [String]
public init(
nameservers: [String] = defaultNameservers,
domain: String? = nil,
searchDomains: [String] = [],
options: [String] = []
) {
self.nameservers = nameservers
self.domain = domain
self.searchDomains = searchDomains
self.options = options
}
}
/// Resources like cpu, memory, and storage quota.
public struct Resources: Sendable, Codable {
/// Number of CPU cores allocated.
public var cpus: Int = 4
/// Memory in bytes allocated.
public var memoryInBytes: UInt64 = 1024.mib()
/// Storage quota/size in bytes.
public var storage: UInt64?
/// Additional CPU cores allocated for VM overhead (guest agent, etc).
public var cpuOverhead: Int = 1
public init() {}
public init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.cpus = try c.decodeIfPresent(Int.self, forKey: .cpus) ?? 4
self.memoryInBytes = try c.decodeIfPresent(UInt64.self, forKey: .memoryInBytes) ?? 1024.mib()
self.storage = try c.decodeIfPresent(UInt64.self, forKey: .storage)
self.cpuOverhead = try c.decodeIfPresent(Int.self, forKey: .cpuOverhead) ?? 1
}
}
public init(
id: String,
image: ImageDescription,
process: ProcessConfiguration
) {
self.id = id
self.image = image
self.initProcess = process
}
}
@@ -0,0 +1,30 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
public struct ContainerCreateOptions: Codable, Sendable {
/// Remove the container and wipe out its data on container stop
public let autoRemove: Bool
/// Override the rootFs with this one other than the image-cloned version
public let rootFsOverride: Filesystem?
public init(autoRemove: Bool, rootFsOverride: Filesystem? = nil) {
self.autoRemove = autoRemove
self.rootFsOverride = rootFsOverride
}
public static let `default` = ContainerCreateOptions(autoRemove: false)
}
@@ -0,0 +1,55 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import Foundation
/// Filters for listing containers.
public struct ContainerListFilters: Sendable, Codable {
public static func exclude(_ str: String) -> String {
"^(?!\(str)$)"
}
/// Filter by container IDs. If non-empty, only containers with matching IDs are returned.
public var ids: [String]
/// Filter by container status.
public var status: RuntimeStatus?
/// Filter by labels. All specified labels must match. Values are treated as regular expressions
/// matched against the container's label value. If a container does not have the specified key,
/// the value is treated as an empty string. This means a positive pattern (e.g. ``^b$``) will
/// exclude containers without the label, while a negation pattern (e.g. ``^(?!b$)``) will
/// include them.
public var labels: [String: String]
/// No filters applied. Will return all containers.
public static let all = ContainerListFilters()
public init(
ids: [String] = [],
status: RuntimeStatus? = nil,
labels: [String: String] = [:]
) {
self.ids = ids
self.status = status
self.labels = labels
}
}
extension ContainerListFilters {
public func withoutMachines() -> ContainerListFilters {
let labels = self.labels.merging([ResourceLabelKeys.plugin: Self.exclude("machine")]) { _, new in new }
return ContainerListFilters(ids: self.ids, status: self.status, labels: labels)
}
}
@@ -0,0 +1,54 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import ContainerizationOCI
import Foundation
/// A snapshot of a container along with its configuration
/// and any runtime state information.
public struct ContainerSnapshot: Codable, Sendable {
/// The configuration of the container.
public var configuration: ContainerConfiguration
/// Identifier of the container.
public var id: String {
configuration.id
}
/// Configured platform for the container.
public var platform: ContainerizationOCI.Platform {
configuration.platform
}
/// The runtime status of the container.
public var status: RuntimeStatus
/// Network interfaces attached to the sandbox that are provided to the container.
public var networks: [Attachment]
/// When the container was started.
public var startedDate: Date?
public init(
configuration: ContainerConfiguration,
status: RuntimeStatus,
networks: [Attachment],
startedDate: Date? = nil
) {
self.configuration = configuration
self.status = status
self.networks = networks
self.startedDate = startedDate
}
}
@@ -0,0 +1,61 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import Foundation
/// Statistics for a container suitable for CLI display.
public struct ContainerStats: Sendable, Codable {
/// Container ID
public var id: String
/// Physical memory usage in bytes
public var memoryUsageBytes: UInt64?
/// Memory limit in bytes
public var memoryLimitBytes: UInt64?
/// CPU usage in microseconds
public var cpuUsageUsec: UInt64?
/// Network received bytes (sum of all interfaces)
public var networkRxBytes: UInt64?
/// Network transmitted bytes (sum of all interfaces)
public var networkTxBytes: UInt64?
/// Block I/O read bytes (sum of all devices)
public var blockReadBytes: UInt64?
/// Block I/O write bytes (sum of all devices)
public var blockWriteBytes: UInt64?
/// Number of processes in the container
public var numProcesses: UInt64?
public init(
id: String,
memoryUsageBytes: UInt64?,
memoryLimitBytes: UInt64?,
cpuUsageUsec: UInt64?,
networkRxBytes: UInt64?,
networkTxBytes: UInt64?,
blockReadBytes: UInt64?,
blockWriteBytes: UInt64?,
numProcesses: UInt64?
) {
self.id = id
self.memoryUsageBytes = memoryUsageBytes
self.memoryLimitBytes = memoryLimitBytes
self.cpuUsageUsec = cpuUsageUsec
self.networkRxBytes = networkRxBytes
self.networkTxBytes = networkTxBytes
self.blockReadBytes = blockReadBytes
self.blockWriteBytes = blockWriteBytes
self.numProcesses = numProcesses
}
}
@@ -0,0 +1,34 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import Foundation
/// The runtime status of a container. Identity-free: the container's id lives
/// on its ``ContainerConfiguration``.
public struct ContainerStatus: Codable, Sendable {
/// The state-machine value for the container (running, stopped, ).
public let state: RuntimeStatus
/// Network attachments provided to the container.
public let networks: [Attachment]
/// When the container was started, if it has been.
public let startedDate: Date?
public init(state: RuntimeStatus, networks: [Attachment], startedDate: Date? = nil) {
self.state = state
self.networks = networks
self.startedDate = startedDate
}
}
@@ -0,0 +1,32 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import Foundation
public struct ContainerStopOptions: Sendable, Codable {
public var timeoutInSeconds: Int32
public var signal: String?
public static let `default` = ContainerStopOptions(
timeoutInSeconds: 5,
signal: nil
)
public init(timeoutInSeconds: Int32, signal: String?) {
self.timeoutInSeconds = timeoutInSeconds
self.signal = signal
}
}
@@ -0,0 +1,195 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import Foundation
import SystemPackage
/// Options to pass to a mount call.
public typealias MountOptions = [String]
extension MountOptions {
/// Returns true if the Filesystem should be consumed as read-only.
public var readonly: Bool {
self.contains("ro")
}
}
/// A host filesystem that will be attached to the sandbox for use.
///
/// A filesystem will be mounted automatically when starting the sandbox
/// or container.
public struct Filesystem: Sendable, Codable {
/// Type of caching to perform at the host level.
public enum CacheMode: Sendable, Codable {
case on
case off
case auto
}
/// Sync mode to perform at the host level.
public enum SyncMode: Sendable, Codable {
case full
case fsync
case nosync
}
/// The type of filesystem attachment for the sandbox.
public enum FSType: Sendable, Codable, Equatable {
package enum VirtiofsType: String, Sendable, Codable, Equatable {
// This is a virtiofs share for the rootfs of a sandbox.
case rootfs
// Data share. This is what all virtiofs shares for anything besides
// the rootfs for a sandbox will be.
case data
}
case block(format: String, cache: CacheMode, sync: SyncMode)
case volume(name: String, format: String, cache: CacheMode, sync: SyncMode)
case virtiofs
case tmpfs
}
/// Type of the filesystem.
public var type: FSType
/// Source of the filesystem.
public var source: String
/// Destination where the filesystem should be mounted.
public var destination: String
/// Mount options applied when mounting the filesystem.
public var options: MountOptions
public init() {
self.type = .tmpfs
self.source = ""
self.destination = ""
self.options = []
}
public init(type: FSType, source: String, destination: String, options: MountOptions) {
self.type = type
self.source = source
self.destination = destination
self.options = options
}
// Defaulting to CachedMode = .on (i.e., cached mode) to fix Linux FS issue when using Virtualization
// * https://github.com/apple/container/issues/614
// * https://github.com/utmapp/UTM/pull/5919
/// A block based filesystem.
public static func block(
format: String, source: String, destination: String, options: MountOptions, cache: CacheMode = .on,
sync: SyncMode = .fsync
) -> Filesystem {
.init(
type: .block(format: format, cache: cache, sync: sync),
source: absoluteFilePath(for: source).string,
destination: destination,
options: options
)
}
/// A named volume filesystem.
public static func volume(
name: String, format: String, source: String, destination: String, options: MountOptions,
cache: CacheMode = .on, sync: SyncMode = .fsync
) -> Filesystem {
.init(
type: .volume(name: name, format: format, cache: cache, sync: sync),
source: absoluteFilePath(for: source).string,
destination: destination,
options: options
)
}
/// A vritiofs backed filesystem providing a directory.
public static func virtiofs(source: String, destination: String, options: MountOptions) -> Filesystem {
.init(
type: .virtiofs,
source: absoluteFilePath(for: source).string,
destination: destination,
options: options
)
}
public static func tmpfs(destination: String, options: MountOptions) -> Filesystem {
.init(
type: .tmpfs,
source: "tmpfs",
destination: destination,
options: options
)
}
/// Returns true if the Filesystem is backed by a block device.
public var isBlock: Bool {
switch type {
case .block(_, _, _): true
case .volume(_, _, _, _): true
default: false
}
}
/// Returns true if the Filesystem is a named volume.
public var isVolume: Bool {
switch type {
case .volume(_, _, _, _): true
default: false
}
}
/// Returns the volume name if this is a volume filesystem, nil otherwise.
public var volumeName: String? {
switch type {
case .volume(let name, _, _, _): name
default: nil
}
}
/// Returns true if the Filesystem is backed by a in-memory mount type.
public var isTmpfs: Bool {
switch type {
case .tmpfs: true
default: false
}
}
/// Returns true if the Filesystem is backed by virtioFS.
public var isVirtiofs: Bool {
switch type {
case .virtiofs: true
default: false
}
}
/// Clone the Filesystem to the provided path.
///
/// This uses `clonefile` to provide a copy-on-write copy of the Filesystem.
public func clone(to: String) throws -> Self {
let fm = FileManager.default
let src = self.source
try fm.copyItem(atPath: src, toPath: to)
return .init(type: self.type, source: to, destination: self.destination, options: self.options)
}
}
private func absoluteFilePath(for source: String) -> FilePath {
let path = FilePath(source)
guard path.isRelative else { return path.lexicallyNormalized() }
return FilePath(FileManager.default.currentDirectoryPath)
.pushing(path)
.lexicallyNormalized()
}
@@ -0,0 +1,82 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import ContainerizationOCI
import Foundation
/// A container as a managed resource. Wraps the persistent
/// ``ContainerConfiguration`` and a runtime ``ContainerStatus``.
public struct ManagedContainer: ManagedResource {
public let configuration: ContainerConfiguration
public let status: ContainerStatus
// MARK: ManagedResource
public var id: String { configuration.id }
public var name: String { configuration.id } // containers have no separate name field today
public var creationDate: Date { configuration.creationDate }
/// Typed labels for conformance / filtering. Drops labels that fail
/// validation; the raw dictionary is preserved on `configuration.labels`
/// and is what gets serialized.
public var labels: ResourceLabels { (try? ResourceLabels(configuration.labels)) ?? .init() }
/// Platform passthrough (parity with the former ContainerSnapshot.platform).
public var platform: ContainerizationOCI.Platform { configuration.platform }
/// Mint ids the way containers actually mint them (lowercased UUID),
/// not the protocol's 64-hex default.
public static func generateId() -> String { UUID().uuidString.lowercased() }
/// Container name rule (mixed case, dots, hyphens, underscores). Duplicated from
/// Utility.validEntityName
public static func nameValid(_ name: String) -> Bool {
let pattern = #"^[a-zA-Z0-9][a-zA-Z0-9_.-]+$"#
return name.range(of: pattern, options: .regularExpression) != nil
}
public init(configuration: ContainerConfiguration, status: ContainerStatus) {
self.configuration = configuration
self.status = status
}
/// CLI-boundary factory: build from the snapshot the client returns today.
public init(_ snapshot: ContainerSnapshot) {
self.configuration = snapshot.configuration
self.status = ContainerStatus(
state: snapshot.status,
networks: snapshot.networks,
startedDate: snapshot.startedDate
)
}
}
extension ManagedContainer {
enum CodingKeys: String, CodingKey { case id, configuration, status }
public func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(id, forKey: .id)
try c.encode(configuration, forKey: .configuration)
try c.encode(status, forKey: .status)
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.configuration = try c.decode(ContainerConfiguration.self, forKey: .configuration)
self.status = try c.decode(ContainerStatus.self, forKey: .status)
}
}
@@ -0,0 +1,92 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
/// Configuration data for an executable Process.
public struct ProcessConfiguration: Sendable, Codable {
/// The on disk path to the executable binary.
public var executable: String
/// Arguments passed to the Process.
public var arguments: [String]
/// Environment variables for the Process.
public var environment: [String]
/// The current working directory (cwd) for the Process.
public var workingDirectory: String
/// A boolean value indicating if a Terminal or PTY device should
/// be attached to the Process's Standard I/O.
public var terminal: Bool
/// The User a Process should execute under.
public var user: User
/// Supplemental groups for the Process.
public var supplementalGroups: [UInt32]
/// Rlimits for the Process.
public var rlimits: [Rlimit]
/// Rlimits for Processes.
public struct Rlimit: Sendable, Codable {
/// The Rlimit type of the Process.
///
/// Values include standard Rlimit resource types, i.e. RLIMIT_NPROC, RLIMIT_NOFILE, ...
public let limit: String
/// The soft limit of the Process
public let soft: UInt64
/// The hard or max limit of the Process.
public let hard: UInt64
public init(limit: String, soft: UInt64, hard: UInt64) {
self.limit = limit
self.soft = soft
self.hard = hard
}
}
/// The User information for a Process.
public enum User: Sendable, Codable, CustomStringConvertible, Equatable {
/// Given the raw user string of the form <uid:gid> or <user:group> or <user> lookup the uid/gid within
/// the container before setting it for the Process.
case raw(userString: String)
/// Set the provided uid/gid for the Process.
case id(uid: UInt32, gid: UInt32)
public var description: String {
switch self {
case .id(let uid, let gid):
return "\(uid):\(gid)"
case .raw(let name):
return name
}
}
}
public init(
executable: String,
arguments: [String],
environment: [String],
workingDirectory: String = "/",
terminal: Bool = false,
user: User = .id(uid: 0, gid: 0),
supplementalGroups: [UInt32] = [],
rlimits: [Rlimit] = []
) {
self.executable = executable
self.arguments = arguments
self.environment = environment
self.workingDirectory = workingDirectory
self.terminal = terminal
self.user = user
self.supplementalGroups = supplementalGroups
self.rlimits = rlimits
}
}
@@ -0,0 +1,109 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import ContainerizationError
import ContainerizationExtras
/// The network protocols available for port forwarding.
public enum PublishProtocol: String, Sendable, Codable {
case tcp = "tcp"
case udp = "udp"
/// Initialize a protocol with to default value, `.tcp`.
public init() {
self = .tcp
}
/// Initialize a protocol value from the provided string.
public init?(_ value: String) {
switch value.lowercased() {
case "tcp": self = .tcp
case "udp": self = .udp
default: return nil
}
}
}
/// Specifies internet port forwarding from host to container.
public struct PublishPort: Sendable, Codable {
/// The IP address of the proxy listener on the host
public let hostAddress: IPAddress
/// The port number of the proxy listener on the host
public let hostPort: UInt16
/// The port number of the container listener
public let containerPort: UInt16
/// The network protocol for the proxy
public let proto: PublishProtocol
/// The number of ports to publish
public let count: UInt16
/// Creates a new port forwarding specification.
public init(
hostAddress: IPAddress,
hostPort: UInt16,
containerPort: UInt16,
proto: PublishProtocol,
count: UInt16
) throws {
self.hostAddress = hostAddress
self.hostPort = hostPort
self.containerPort = containerPort
self.proto = proto
self.count = count
try validatePortRange(port: hostPort, count: count)
try validatePortRange(port: containerPort, count: count)
}
/// Create a configuration from the supplied Decoder, initializing missing
/// values where possible to reasonable defaults.
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
hostAddress = try container.decode(IPAddress.self, forKey: .hostAddress)
hostPort = try container.decode(UInt16.self, forKey: .hostPort)
containerPort = try container.decode(UInt16.self, forKey: .containerPort)
proto = try container.decode(PublishProtocol.self, forKey: .proto)
count = try container.decodeIfPresent(UInt16.self, forKey: .count) ?? 1
try validatePortRange(port: hostPort, count: count)
try validatePortRange(port: containerPort, count: count)
}
private func validatePortRange(port: UInt16, count: UInt16) throws {
guard count > 0, UInt16.max - port >= count - 1 else {
throw ContainerizationError(.invalidArgument, message: "invalid port and count: \(port), \(count)")
}
}
}
extension [PublishPort] {
public func hasOverlaps() -> Bool {
var hostPorts = Set<String>()
for publishPort in self {
for offset in 0..<publishPort.count {
let hostPortKey = "\(publishPort.hostPort + offset)/\(publishPort.proto.rawValue)"
guard !hostPorts.contains(hostPortKey) else {
return true
}
hostPorts.insert(hostPortKey)
}
}
return false
}
}
@@ -0,0 +1,161 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import ContainerizationError
import Foundation
import SystemPackage
/// Represents a socket that should be published from container to host.
///
/// - Deprecated: New for 1.0.0, path types changed from `URL` to `FilePath`.
/// - Note: Decoder handles `FilePath` and `URL` for persistent data compatibility;
/// this compatibility will be removed in a later release.
public struct PublishSocket: Sendable, Codable {
/// Absolute path to the socket inside the container.
public var containerPath: FilePath
/// Absolute path where the socket appears on the host.
public var hostPath: FilePath
/// File permissions for the socket on the host.
public var permissions: FilePermissions?
/// Creates a `PublishSocket` with validated absolute paths.
///
/// - Parameters:
/// - containerPath: Absolute path to the socket inside the container.
/// Must begin with `/`.
/// - hostPath: Absolute path where the socket appears on the host.
/// Must begin with `/`.
/// - permissions: File permissions applied to the socket on the host.
/// - Throws: `ContainerizationError` with code `.invalidArgument` if
/// either path is not absolute.
public init(
containerPath: FilePath,
hostPath: FilePath,
permissions: FilePermissions? = nil
) throws {
guard containerPath.isAbsolute else {
throw ContainerizationError(
.invalidArgument,
message: "containerPath must be absolute: \(containerPath)"
)
}
guard hostPath.isAbsolute else {
throw ContainerizationError(
.invalidArgument,
message: "hostPath must be absolute: \(hostPath)"
)
}
self.containerPath = containerPath
self.hostPath = hostPath
self.permissions = permissions
}
private enum CodingKeys: String, CodingKey {
case containerPath
case hostPath
case permissions
}
/// Encodes each path as its plain absolute string (e.g. `"/var/run/docker.sock"`).
///
/// Pre-1.0 wire-format change from the prior `URL`-typed encoding which
/// emitted `URL.absoluteString` (`"file:///var/run/docker.sock"`). The
/// decoder accepts both forms for compatibility with persisted bundles
/// from earlier releases; that compatibility will be removed in a later
/// release.
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(containerPath.string, forKey: .containerPath)
try container.encode(hostPath.string, forKey: .hostPath)
try container.encodeIfPresent(permissions, forKey: .permissions)
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let containerPath = try Self.decodePath(from: container, forKey: .containerPath)
let hostPath = try Self.decodePath(from: container, forKey: .hostPath)
let permissions = try container.decodeIfPresent(FilePermissions.self, forKey: .permissions)
do {
try self.init(
containerPath: containerPath,
hostPath: hostPath,
permissions: permissions
)
} catch let error as ContainerizationError {
throw DecodingError.dataCorruptedError(
forKey: .containerPath,
in: container,
debugDescription: String(describing: error)
)
}
}
/// Decodes a `FilePath` accepting either the new plain-path form
/// (`"/var/run/docker.sock"`) or the legacy file-URL form emitted by
/// older releases (`"file:///var/run/docker.sock"`). Throws
/// `DecodingError.dataCorrupted` on a malformed file URL, empty input, or a
/// non-absolute path validating decoded paths here guards against
/// manually edited or corrupt persisted configs, complementing the
/// by-construction check in `init(containerPath:hostPath:permissions:)`.
private static func decodePath(
from container: KeyedDecodingContainer<CodingKeys>,
forKey key: CodingKeys
) throws -> FilePath {
let raw = try container.decode(String.self, forKey: key)
let path: String
if raw.hasPrefix("file:") {
guard let url = URL(string: raw), url.isFileURL else {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "malformed file URL: \(raw)"
)
}
if let host = url.host(), !host.isEmpty, host != "localhost" {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "file URL host must be empty or 'localhost': \(raw)"
)
}
path = url.path(percentEncoded: false)
} else {
path = raw
}
guard !path.isEmpty else {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "decoded socket path is empty: \(raw)"
)
}
let filePath = FilePath(path)
guard filePath.isAbsolute else {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "decoded socket path must be absolute: \(raw)"
)
}
return filePath
}
}
@@ -0,0 +1,29 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import Foundation
/// Runtime status for a sandbox or container.
public enum RuntimeStatus: String, CaseIterable, Sendable, Codable {
/// The object is in an unknown status.
case unknown
/// The object is currently stopped.
case stopped
/// The object is currently running.
case running
/// The object is currently stopping.
case stopping
}