chore: import upstream snapshot with attribution
container project - merge build / Invoke build (push) Successful in 1s
container project - merge build / Invoke build (push) Successful in 1s
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user