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,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
|
||||
}
|
||||
Reference in New Issue
Block a user