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,36 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
/// Protocol for errors with a stable code and structured metadata.
/// This allows the client to present the error as it chooses.
import OrderedCollections
public protocol AppError: Error {
var code: AppErrorCode { get }
var metadata: OrderedDictionary<String, String> { get }
var underlyingError: Error? { get }
}
public struct AppErrorCode: RawRepresentable, Hashable, Sendable {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
public static let invalidArgument = AppErrorCode(rawValue: "invalid_argument")
}
@@ -0,0 +1,47 @@
//===----------------------------------------------------------------------===//
// 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
extension FileManager {
/// Total bytes allocated on disk for all files in a directory (recursive).
///
/// Caveats: hidden files are skipped, symlinks to directories are not followed, but
/// symlinks-to-files and hard links each contribute their target's full allocation
/// so shared inodes are counted multiple times.
public func allocatedSize(of directory: URL) -> UInt64 {
guard
let enumerator = self.enumerator(
at: directory,
includingPropertiesForKeys: [.totalFileAllocatedSizeKey],
options: [.skipsHiddenFiles]
)
else {
return 0
}
var size: UInt64 = 0
for case let fileURL as URL in enumerator {
guard let resourceValues = try? fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey]),
let fileSize = resourceValues.totalFileAllocatedSize
else {
continue
}
size += UInt64(fileSize)
}
return size
}
}
@@ -0,0 +1,60 @@
//===----------------------------------------------------------------------===//
// 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
/// Common properties for all managed resources.
public protocol ManagedResource: Identifiable, Sendable, Codable {
/// A 64 byte hexadecimal string, assigned by the system, that uniquely
/// identifies the resource.
var id: String { get }
/// A user assigned name that shall be unique within the namespace of
/// the resource category. If the user does not assign a name, this value
/// shall be the same as the system-assigned identifier.
var name: String { get }
/// The time at which the system created the resource.
var creationDate: Date { get }
/// Key-value properties for the resource. The user and system may both
/// make use of labels to read and write annotations or other metadata.
/// A good practice when using labels for automation is to use reverse
/// domain name notation (example: `com.example.mytool.role`) for
/// label names.
var labels: ResourceLabels { get }
/// Generates a unique resource ID value.
static func generateId() -> String
/// Returns true only if the specified resource name is syntactically valid.
static func nameValid(_ name: String) -> Bool
}
extension ManagedResource {
/// Generate a random identifier that has the format of an ASCII SHA-256 hash.
public static func generateId() -> String {
(0..<2)
.map { _ in UInt128.random(in: 0...UInt128.max) }
.map { String($0, radix: 16).padding(toLength: 32, withPad: "0", startingAt: 0) }
.joined()
}
}
extension ResourceLabels {
/// Returns true if for a resource that the system automatically manages.
public var isBuiltin: Bool { self.contains { $0 == ResourceLabelKeys.role && $1 == ResourceRoleValues.builtin } }
}
@@ -0,0 +1,117 @@
//===----------------------------------------------------------------------===//
// 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 OrderedCollections
/// Metadata for a managed resource.
public struct ResourceLabels: Sendable, Equatable {
public static let keyLengthMax = 128
public static let labelLengthMax = 4096
public let dictionary: [String: String]
public struct LabelError: AppError {
public var code: AppErrorCode
public var metadata: OrderedDictionary<String, String>
public var underlyingError: (any Error)? { nil }
}
public init() {
dictionary = [:]
}
public init(_ labels: [String: String]) throws {
for (key, value) in labels {
try Self.validateLabel(key: key, value: value)
}
self.dictionary = labels
}
public static func validateLabelKey(_ key: String) throws {
guard key.count <= Self.keyLengthMax else {
throw LabelError(code: .invalidLabelKeyLength, metadata: ["key": key, "maxLength": "\(Self.keyLengthMax)"])
}
let dockerPattern = #/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*$/#
let ociPattern = #/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?:/(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*))*$/#
let dockerMatch = !key.ranges(of: dockerPattern).isEmpty
let ociMatch = !key.ranges(of: ociPattern).isEmpty
guard dockerMatch || ociMatch else {
throw LabelError(code: .invalidLabelKeyContent, metadata: ["key": key])
}
}
public static func validateLabel(key: String, value: String) throws {
try validateLabelKey(key)
let fullLabel = "\(key)=\(value)"
guard fullLabel.count <= labelLengthMax else {
throw LabelError(code: .invalidLabelLength, metadata: ["label": fullLabel, "maxLength": "\(Self.labelLengthMax)"])
}
}
}
extension ResourceLabels: Codable {
public func encode(to encoder: Encoder) throws {
try dictionary.encode(to: encoder)
}
public init(from decoder: Decoder) throws {
let dict = try [String: String](from: decoder)
try self.init(dict)
}
}
extension ResourceLabels: Collection {
public typealias Index = Dictionary<String, String>.Index
public typealias Element = Dictionary<String, String>.Element
public var startIndex: Index { dictionary.startIndex }
public var endIndex: Index { dictionary.endIndex }
public subscript(position: Index) -> Element { dictionary[position] }
public func index(after i: Index) -> Index { dictionary.index(after: i) }
// Direct key access
public subscript(key: String) -> String? {
get { dictionary[key] }
}
}
extension AppErrorCode {
public static let invalidLabelKeyContent = AppErrorCode(rawValue: "invalid_label_key_content")
public static let invalidLabelKeyLength = AppErrorCode(rawValue: "invalid_label_key_length")
public static let invalidLabelLength = AppErrorCode(rawValue: "invalid_label_length")
}
/// System-defined keys for resource labels.
public struct ResourceLabelKeys {
/// Indicates a owner of a resource managed by a plugin.
public static let plugin = "com.apple.container.plugin"
/// Indicates a resource with a reserved or dedicated purpose.
public static let role = "com.apple.container.resource.role"
}
/// System-defined values for resource the resource role label.
public struct ResourceRoleValues {
/// Indicates a container that can build images.
public static let builder = "builder"
/// Indicates a system-created resource that cannot be deleted by the user.
public static let builtin = "builtin"
}
@@ -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
}
@@ -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 ContainerizationError
import ContainerizationOCI
/// A type that represents an OCI image that can be used with sandboxes or containers.
public struct ImageDescription: Sendable, Codable {
/// The public reference/name of the image.
public let reference: String
/// The descriptor of the image.
public let descriptor: Descriptor
public var digest: String { descriptor.digest }
public var mediaType: String { descriptor.mediaType }
public init(reference: String, descriptor: Descriptor) {
self.reference = reference
self.descriptor = descriptor
}
}
@@ -0,0 +1,133 @@
//===----------------------------------------------------------------------===//
// 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
/// An image resource, representing an OCI image managed by the system.
///
/// `ImageResource` conforms to `ManagedResource` and wraps the image's
/// ``ImageDescription`` (its reference and index descriptor) alongside the
/// resolved index descriptor and the per-platform variants that make up the
/// image.
public struct ImageResource: ManagedResource {
/// A single platform-specific variant of an image.
public struct Variant: Sendable, Codable {
/// The platform this variant targets.
public let platform: Platform
/// The digest of this variant's manifest.
public let digest: String
/// The total size of this variant in bytes.
public let size: Int64
/// The OCI image config for this variant.
public let config: ContainerizationOCI.Image
public init(platform: Platform, digest: String, size: Int64, config: ContainerizationOCI.Image) {
self.platform = platform
self.digest = digest
self.size = size
self.config = config
}
}
public struct ImageConfiguration: Sendable, Codable {
public let creationDate: Date
public var name: String
public var descriptor: Descriptor
public init(description: ImageDescription, creationDate: Date) {
self.creationDate = creationDate
self.name = description.reference
self.descriptor = description.descriptor
}
}
/// The image's description its reference and index descriptor.
public let configuration: ImageConfiguration
/// The platform-specific variants contained in the image.
public let variants: [Variant]
/// The reference to show in human-facing listings, with default-registry
/// information removed (e.g. `alpine` rather than `docker.io/library/alpine`).
/// Computed by the caller, which has access to the system configuration.
/// Defaults to the full ``name`` when not supplied.
public let displayReference: String
// MARK: ManagedResource
/// The scheme-specific value of `configuration.descriptor.digest` (the hex portion
/// after the algorithm prefix). The fully-qualified digest needed for content-store
/// lookups and XPC transport is always recoverable as `configuration.descriptor.digest`.
public var id: String {
let digest = configuration.descriptor.digest
guard let colonIndex = digest.firstIndex(of: ":") else { return digest }
return String(digest[digest.index(after: colonIndex)...])
}
/// The user-facing reference (`name:tag`) for this image.
public var name: String { configuration.name }
/// The time at which the image was created, resolved from the OCI image
/// config. Falls back to the Unix epoch when no creation date is recorded.
public var creationDate: Date { configuration.creationDate }
/// Key-value labels for this image, derived from the index descriptor's
/// annotations. Returns an empty label set if the annotations fail
/// ``ResourceLabels`` validation.
public var labels: ResourceLabels {
(try? ResourceLabels(configuration.descriptor.annotations ?? [:])) ?? ResourceLabels()
}
// MARK: Initialization
public init(configuration: ImageConfiguration, variants: [Variant], displayReference: String? = nil) {
self.configuration = configuration
self.variants = variants
self.displayReference = displayReference ?? configuration.name
}
}
extension ImageResource {
/// Returns `true` if `name` is a syntactically valid image reference.
public static func nameValid(_ name: String) -> Bool {
(try? Reference.parse(name)) != nil
}
}
// MARK: - Codable
extension ImageResource {
enum CodingKeys: String, CodingKey {
case id
case configuration
case variants
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(configuration, forKey: .configuration)
try container.encode(variants, forKey: .variants)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.configuration = try container.decode(ImageConfiguration.self, forKey: .configuration)
self.variants = try container.decode([Variant].self, forKey: .variants)
// `displayReference` is a display-only value and is not serialized.
self.displayReference = self.configuration.name
}
}
@@ -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 ContainerizationExtras
/// A snapshot of a network interface for a sandbox.
public struct Attachment: Codable, Sendable {
/// The network ID associated with the attachment.
public let network: String
/// The hostname associated with the attachment.
public let hostname: String
/// The CIDR address describing the interface IPv4 address, with the prefix length of the subnet.
public let ipv4Address: CIDRv4
/// The IPv4 gateway address.
public let ipv4Gateway: IPv4Address
/// The CIDR address describing the interface IPv6 address, with the prefix length of the subnet.
/// The address is nil if the IPv6 subnet could not be determined at network creation time.
public let ipv6Address: CIDRv6?
/// The MAC address associated with the attachment (optional).
public let macAddress: MACAddress?
/// The MTU for the network interface.
public let mtu: UInt32?
/// The network plugin variant, used by the runtime to select an interface strategy.
public let variant: String?
public init(
network: String,
hostname: String,
ipv4Address: CIDRv4,
ipv4Gateway: IPv4Address,
ipv6Address: CIDRv6?,
macAddress: MACAddress?,
mtu: UInt32? = nil,
variant: String? = nil
) {
self.network = network
self.hostname = hostname
self.ipv4Address = ipv4Address
self.ipv4Gateway = ipv4Gateway
self.ipv6Address = ipv6Address
self.macAddress = macAddress
self.mtu = mtu
self.variant = variant
}
enum CodingKeys: String, CodingKey {
case network
case hostname
case ipv4Address
case ipv4Gateway
case ipv6Address
case macAddress
case mtu
case variant
// TODO: retain for deserialization compatibility for now, remove later
case address
case gateway
}
/// 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)
network = try container.decode(String.self, forKey: .network)
hostname = try container.decode(String.self, forKey: .hostname)
if let address = try? container.decode(CIDRv4.self, forKey: .ipv4Address) {
ipv4Address = address
} else {
ipv4Address = try container.decode(CIDRv4.self, forKey: .address)
}
if let gateway = try? container.decode(IPv4Address.self, forKey: .ipv4Gateway) {
ipv4Gateway = gateway
} else {
ipv4Gateway = try container.decode(IPv4Address.self, forKey: .gateway)
}
ipv6Address = try container.decodeIfPresent(CIDRv6.self, forKey: .ipv6Address)
macAddress = try container.decodeIfPresent(MACAddress.self, forKey: .macAddress)
mtu = try container.decodeIfPresent(UInt32.self, forKey: .mtu)
variant = try container.decodeIfPresent(String.self, forKey: .variant)
}
/// Encode the configuration to the supplied Encoder.
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(network, forKey: .network)
try container.encode(hostname, forKey: .hostname)
try container.encode(ipv4Address, forKey: .ipv4Address)
try container.encode(ipv4Gateway, forKey: .ipv4Gateway)
try container.encodeIfPresent(ipv6Address, forKey: .ipv6Address)
try container.encodeIfPresent(macAddress, forKey: .macAddress)
try container.encodeIfPresent(mtu, forKey: .mtu)
try container.encodeIfPresent(variant, forKey: .variant)
}
}
@@ -0,0 +1,49 @@
//===----------------------------------------------------------------------===//
// 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 ContainerizationExtras
/// Configuration information for attaching a container network interface to a network.
public struct AttachmentConfiguration: Codable, Sendable {
/// The network ID associated with the attachment.
public let network: String
/// The option information for the attachment
public let options: AttachmentOptions
public init(network: String, options: AttachmentOptions) {
self.network = network
self.options = options
}
}
// Option information for a network attachment.
public struct AttachmentOptions: Codable, Sendable {
/// The hostname associated with the attachment.
public let hostname: String
/// The MAC address associated with the attachment (optional).
public let macAddress: MACAddress?
/// The MTU for the network interface.
public let mtu: UInt32?
public init(hostname: String, macAddress: MACAddress? = nil, mtu: UInt32? = nil) {
self.hostname = hostname
self.macAddress = macAddress
self.mtu = mtu
}
}
@@ -0,0 +1,151 @@
//===----------------------------------------------------------------------===//
// 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
import Foundation
/// Configuration parameters for network creation.
public struct NetworkConfiguration: Codable, Sendable, Identifiable {
/// The name of the network.
public let name: String
/// The unique identifier for the network. Identical to ``name``.
public var id: String { name }
/// The network type
public let mode: NetworkMode
/// When the network was created.
public let creationDate: Date
/// The preferred CIDR address for the IPv4 subnet, if specified
public let ipv4Subnet: CIDRv4?
/// The preferred CIDR address for the IPv6 subnet, if specified
public let ipv6Subnet: CIDRv6?
/// Key-value labels for the network.
/// Resource labels should not be mutated, except while building a network configurations.
public let labels: ResourceLabels
/// The network plugin that manages this network.
public let plugin: String
/// Plugin-specific options for this network.
public let options: [String: String]
/// Creates a network configuration
public init(
name: String,
mode: NetworkMode,
ipv4Subnet: CIDRv4? = nil,
ipv6Subnet: CIDRv6? = nil,
labels: ResourceLabels = .init(),
plugin: String,
options: [String: String] = [:]
) throws {
self.name = name
self.creationDate = Date()
self.mode = mode
self.ipv4Subnet = ipv4Subnet
self.ipv6Subnet = ipv6Subnet
self.labels = labels
self.plugin = plugin
self.options = options
try validate()
}
enum CodingKeys: String, CodingKey {
case name
// Deprecated: As of 1.0.0. Use ``name`` instead of ``id``.
// Note: Will be removed in a later release.
case id
case creationDate
case mode
case ipv4Subnet
case ipv6Subnet
case labels
case plugin
case options
// TODO: retain for deserialization compatibility, remove in next major version
case pluginInfo
case subnet
}
/// 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)
name =
try container.decodeIfPresent(String.self, forKey: .name)
?? container.decode(String.self, forKey: .id)
creationDate = try container.decodeIfPresent(Date.self, forKey: .creationDate) ?? Date(timeIntervalSince1970: 0)
mode = try container.decode(NetworkMode.self, forKey: .mode)
let subnetText =
try container.decodeIfPresent(String.self, forKey: .ipv4Subnet)
?? container.decodeIfPresent(String.self, forKey: .subnet)
ipv4Subnet = try subnetText.map { try CIDRv4($0) }
ipv6Subnet = try container.decodeIfPresent(String.self, forKey: .ipv6Subnet)
.map { try CIDRv6($0) }
let decodedLabels = try container.decodeIfPresent([String: String].self, forKey: .labels) ?? [:]
labels = try .init(decodedLabels)
if let plugin = try container.decodeIfPresent(String.self, forKey: .plugin) {
self.plugin = plugin
self.options = try container.decodeIfPresent([String: String].self, forKey: .options) ?? [:]
} else if let legacy = try container.decodeIfPresent(_LegacyPluginInfo.self, forKey: .pluginInfo) {
// Deprecated: As of 1.0.0. Use ``plugin`` and ``options`` instead.
// Note: Will be removed in a later release.
self.plugin = legacy.plugin
var opts: [String: String] = [:]
if let variant = legacy.variant { opts["variant"] = variant }
self.options = opts
} else {
self.plugin = "container-network-vmnet"
self.options = [:]
}
try validate()
}
/// Encode the configuration to the supplied Encoder.
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(creationDate, forKey: .creationDate)
try container.encode(mode, forKey: .mode)
try container.encodeIfPresent(ipv4Subnet, forKey: .ipv4Subnet)
try container.encodeIfPresent(ipv6Subnet, forKey: .ipv6Subnet)
try container.encode(labels, forKey: .labels)
try container.encode(plugin, forKey: .plugin)
try container.encode(options, forKey: .options)
}
private func validate() throws {
guard NetworkResource.nameValid(name) else {
throw ContainerizationError(.invalidArgument, message: "invalid network name: \(name)")
}
}
}
/// Decode helper for stored configurations that used the old `pluginInfo` key.
private struct _LegacyPluginInfo: Codable {
let plugin: String
let variant: String?
}
@@ -0,0 +1,41 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
/// Networking mode that applies to client containers.
public enum NetworkMode: String, Codable, Sendable {
/// NAT networking mode.
/// Containers do not have routable IPs, and the host performs network
/// address translation to allow containers to reach external services.
case nat = "nat"
/// Host only networking mode
/// Containers can talk with each other in the same subnet only.
case hostOnly = "hostOnly"
}
extension NetworkMode {
public init() {
self = .nat
}
public init?(_ value: String) {
switch value.lowercased() {
case "nat": self = .nat
case "hostOnly": self = .hostOnly
default: return nil
}
}
}
@@ -0,0 +1,96 @@
//===----------------------------------------------------------------------===//
// 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 ContainerizationExtras
import Foundation
/// A network resource, representing a configured virtual network and its runtime status.
///
/// `NetworkResource` conforms to `ManagedResource` and separates the network's
/// intrinsic configuration from its runtime status following the same config/status
/// split used by Kubernetes and Docker. `configuration` is persisted; `status` reflects
/// what the network plugin reports at runtime.
///
/// JSON encoding produces three top-level keys: `id`, `configuration` (the persistent
/// config), and `status` (runtime address properties assigned by the network plugin).
public struct NetworkResource: ManagedResource {
/// The network's configuration its persistent, intrinsic properties.
public let configuration: NetworkConfiguration
/// The network's runtime status the addresses assigned by the network plugin.
public let status: NetworkStatus
// MARK: ManagedResource
/// The unique identifier for this network. Identical to ``configuration/name``.
public var id: String { configuration.name }
/// The user-assigned name for this network. For networks, name and ID are the same.
public var name: String { configuration.name }
/// The time at which this network was created.
public var creationDate: Date { configuration.creationDate }
/// Key-value labels for this network.
public var labels: ResourceLabels { configuration.labels }
/// Returns `true` for a system-managed network that cannot be deleted by the user.
public var isBuiltin: Bool { labels.isBuiltin }
/// Returns `true` if `name` is a syntactically valid network identifier.
///
/// Valid network names are lowercase alphanumeric strings of up to 63
/// characters, allowing dots, hyphens, and underscores in interior positions.
public static func nameValid(_ name: String) -> Bool {
let pattern = #"^[a-z0-9](?:[a-z0-9._-]{0,61}[a-z0-9])?$"#
return name.range(of: pattern, options: .regularExpression) != nil
}
// MARK: Initialization
/// Creates a network resource.
///
/// - Parameters:
/// - configuration: The network's intrinsic configuration.
/// - status: The runtime status reported by the network plugin.
public init(configuration: NetworkConfiguration, status: NetworkStatus) {
self.configuration = configuration
self.status = status
}
}
// MARK: - Codable
extension NetworkResource {
enum CodingKeys: String, CodingKey {
case id
case configuration
case status
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(configuration, forKey: .configuration)
try container.encode(status, forKey: .status)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
configuration = try container.decode(NetworkConfiguration.self, forKey: .configuration)
status = try container.decode(NetworkStatus.self, forKey: .status)
}
}
@@ -0,0 +1,40 @@
//===----------------------------------------------------------------------===//
// 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 ContainerizationExtras
/// The runtime status of a network the addresses assigned once the network
/// plugin is active. Only present after the network has started.
public struct NetworkStatus: Codable, Sendable {
/// The IPv4 subnet assigned to the network.
public let ipv4Subnet: CIDRv4
/// The IPv4 gateway address.
public let ipv4Gateway: IPv4Address
/// The IPv6 subnet assigned to the network, if IPv6 is enabled.
public let ipv6Subnet: CIDRv6?
public init(
ipv4Subnet: CIDRv4,
ipv4Gateway: IPv4Address,
ipv6Subnet: CIDRv6?
) {
self.ipv4Subnet = ipv4Subnet
self.ipv4Gateway = ipv4Gateway
self.ipv6Subnet = ipv6Subnet
}
}
@@ -0,0 +1,120 @@
//===----------------------------------------------------------------------===//
// 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 ContainerizationOS
import Foundation
/// A container registry resource representing a configured registry endpoint.
///
/// Registry resources store authentication and configuration information for
/// container registries such as Docker Hub, GitHub Container Registry, or
/// private registries.
public struct RegistryResource: ManagedResource {
/// The registry hostname that uniquely identifies this resource.
///
/// For registry resources, the identifier is the same as the hostname.
public let id: String
/// The hostname of the registry.
///
/// This value must be a valid DNS hostname or IPv6 address, optionally
/// followed by a port number (e.g., "docker.io", "localhost:5000", "[::1]:5000").
public let name: String
/// The username used for authentication with this registry.
public let username: String
/// The time at which the system created this registry resource.
public let creationDate: Date
/// The time at which the registry resource was last modified.
public let modificationDate: Date
/// Key-value properties for the resource.
///
/// The user and system may both make use of labels to read and write
/// annotations or other metadata.
public let labels: ResourceLabels
/// Validates a registry hostname according to OCI distribution specification.
///
/// This method validates that a registry hostname conforms to the domain pattern
/// used by OCI image references. It supports DNS hostnames, IPv6 addresses, and
/// optional port numbers.
///
/// - Parameter name: The registry hostname to validate
/// - Returns: `true` if the hostname is syntactically valid, `false` otherwise
///
/// ## Valid Examples
/// - `docker.io`
/// - `registry.example.com`
/// - `localhost:5000`
/// - `[::1]:5000`
///
/// ## Implementation Notes
/// The validation logic is based on ContainerizationOCI's `Reference.domainPattern`.
/// See <https://github.com/apple/containerization/blob/main/Sources/ContainerizationOCI/Reference.swift>
public static func nameValid(_ name: String) -> Bool {
// Domain validation logic based on ContainerizationOCI Reference.domainPattern
// See: https://github.com/apple/containerization/blob/main/Sources/ContainerizationOCI/Reference.swift
// TODO: if we have domain IP validation API, use that instead
let domainNameComponent = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])"
let optionalPort = "(?::[0-9]+)?"
let ipv6address = "\\[(?:[a-fA-F0-9:]+)\\]"
let domainName = "\(domainNameComponent)(?:\\.\(domainNameComponent))*"
let host = "(?:\(domainName)|\(ipv6address))"
let pattern = "^\(host)\(optionalPort)$"
return name.range(of: pattern, options: .regularExpression) != nil
}
/// Creates a new registry resource.
///
/// - Parameters:
/// - hostname: The registry hostname (also used as the resource ID)
/// - username: The username for authentication
/// - creationDate: The time the resource was created
/// - modificationDate: The time the resource was last modified
/// - labels: Optional key-value labels for metadata (default: empty dictionary)
public init(
hostname: String,
username: String,
creationDate: Date,
modificationDate: Date,
labels: ResourceLabels = .init()
) {
self.id = hostname
self.name = hostname
self.username = username
self.creationDate = creationDate
self.modificationDate = modificationDate
self.labels = labels
}
}
extension RegistryResource {
/// Creates a registry resource from registry information.
///
/// - Parameter registryInfo: The registry information to convert
public init(from registryInfo: RegistryInfo) {
self.init(
hostname: registryInfo.hostname,
username: registryInfo.username,
creationDate: registryInfo.createdDate,
modificationDate: registryInfo.modifiedDate
)
}
}
@@ -0,0 +1,156 @@
//===----------------------------------------------------------------------===//
// 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
/// A named or anonymous volume that can be mounted in containers.
public struct VolumeConfiguration: Sendable, Equatable, Identifiable {
// id of the volume.
public var id: String { name }
// Name of the volume.
public var name: String
// Driver used to create the volume.
public var driver: String
// Filesystem format of the volume.
public var format: String
// The mount point of the volume on the host.
public var source: String
// Timestamp when the volume was created.
public var creationDate: Date
// User-defined key/value metadata.
public var labels: [String: String]
// Driver-specific options.
public var options: [String: String]
// Size of the volume in bytes (optional).
public var sizeInBytes: UInt64?
public init(
name: String,
driver: String = "local",
format: String = "ext4",
source: String,
creationDate: Date = Date(),
labels: [String: String] = [:],
options: [String: String] = [:],
sizeInBytes: UInt64? = nil
) {
self.name = name
self.driver = driver
self.format = format
self.source = source
self.creationDate = creationDate
self.labels = labels
self.options = options
self.sizeInBytes = sizeInBytes
}
enum CodingKeys: String, CodingKey {
case name, driver, format, source, labels, options, sizeInBytes
case creationDate
// TODO: retain for deserialization compatibility, remove in next major version
case createdAt
}
}
extension VolumeConfiguration: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
driver = try container.decode(String.self, forKey: .driver)
format = try container.decode(String.self, forKey: .format)
source = try container.decode(String.self, forKey: .source)
// Deprecated: As of 1.0.0. Use ``creationDate`` instead of ``createdAt``.
// Note: Will be removed in a later release.
creationDate =
try container.decodeIfPresent(Date.self, forKey: .creationDate)
?? container.decodeIfPresent(Date.self, forKey: .createdAt)
?? Date(timeIntervalSince1970: 0)
labels = try container.decodeIfPresent([String: String].self, forKey: .labels) ?? [:]
options = try container.decodeIfPresent([String: String].self, forKey: .options) ?? [:]
sizeInBytes = try container.decodeIfPresent(UInt64.self, forKey: .sizeInBytes)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(driver, forKey: .driver)
try container.encode(format, forKey: .format)
try container.encode(source, forKey: .source)
try container.encode(creationDate, forKey: .creationDate)
try container.encode(labels, forKey: .labels)
try container.encode(options, forKey: .options)
try container.encodeIfPresent(sizeInBytes, forKey: .sizeInBytes)
}
}
extension VolumeConfiguration {
/// Reserved label key for marking anonymous volumes
public static let anonymousLabel = "com.apple.container.resource.anonymous"
/// Whether this is an anonymous volume (detected via label)
public var isAnonymous: Bool {
labels[Self.anonymousLabel] != nil
}
}
/// Error types for volume operations.
public enum VolumeError: Error, LocalizedError {
case volumeNotFound(String)
case volumeAlreadyExists(String)
case volumeInUse(String)
case invalidVolumeName(String)
case driverNotSupported(String)
case storageError(String)
public var errorDescription: String? {
switch self {
case .volumeNotFound(let name):
return "volume '\(name)' not found"
case .volumeAlreadyExists(let name):
return "volume '\(name)' already exists"
case .volumeInUse(let name):
return "volume '\(name)' is currently in use and cannot be accessed by another container, or deleted"
case .invalidVolumeName(let name):
return "invalid volume name '\(name)'"
case .driverNotSupported(let driver):
return "volume driver '\(driver)' is not supported"
case .storageError(let message):
return "storage error: \(message)"
}
}
}
/// Volume storage management utilities.
public struct VolumeStorage {
public static let volumeNamePattern = "^[A-Za-z0-9][A-Za-z0-9_.-]*$"
public static let defaultVolumeSizeBytes: UInt64 = 512 * 1024 * 1024 * 1024 // 512GB
public static func isValidVolumeName(_ name: String) -> Bool {
guard name.count <= 255 else { return false }
do {
let regex = try Regex(volumeNamePattern)
return (try? regex.wholeMatch(in: name)) != nil
} catch {
return false
}
}
/// Generates an anonymous volume name with UUID format
public static func generateAnonymousVolumeName() -> String {
UUID().uuidString.lowercased()
}
}
@@ -0,0 +1,90 @@
//===----------------------------------------------------------------------===//
// 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
/// A volume resource, representing a configured volume.
public struct VolumeResource: ManagedResource {
/// The volume's configuration its persistent, intrinsic properties.
public let configuration: VolumeConfiguration
// MARK: ManagedResource
/// The unique identifier for this volume. Identical to ``VolumeConfiguration/name``.
public var id: String { configuration.name }
/// The user-assigned name for this volume. For volumes, name and ID are the same.
public var name: String { configuration.name }
/// The time at which this volume was created.
public var creationDate: Date { configuration.creationDate }
/// Key-value labels for this volume. If the underlying
/// ``VolumeConfiguration/labels`` dictionary contains values that fail
/// ``ResourceLabels`` validation, this returns an empty label set.
public var labels: ResourceLabels {
(try? ResourceLabels(configuration.labels)) ?? ResourceLabels()
}
/// Whether this is an anonymous volume (detected via the configuration's labels).
public var isAnonymous: Bool { configuration.isAnonymous }
// MARK: Initialization
/// Creates a volume resource.
///
/// - Parameters:
/// - configuration: The volume's intrinsic configuration.
public init(configuration: VolumeConfiguration) {
self.configuration = configuration
}
}
extension VolumeResource {
public static let volumeNamePattern = "^[A-Za-z0-9][A-Za-z0-9_.-]*$"
/// Returns `true` if `name` is a syntactically valid volume identifier.
public static func nameValid(_ name: String) -> Bool {
guard name.count <= 255 else { return false }
do {
let regex = try Regex(volumeNamePattern)
return (try? regex.wholeMatch(in: name)) != nil
} catch {
return false
}
}
}
// MARK: - Codable
extension VolumeResource {
enum CodingKeys: String, CodingKey {
case id
case configuration
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(configuration, forKey: .configuration)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.configuration = try container.decode(VolumeConfiguration.self, forKey: .configuration)
}
}