mirror of
https://github.com/apple/container.git
synced 2026-07-13 13:07:06 +00:00
8e9670c8f8
Co-authored-by: Aditya Ramani <a_ramani@apple.com> Co-authored-by: Agam Dua <agam_dua@apple.com> Co-authored-by: Danny Canter <danny_canter@apple.com> Co-authored-by: Dmitry Kovba <dkovba@apple.com> Co-authored-by: Eric Ernst <eric_ernst@apple.com> Co-authored-by: Evan Hazlett <ehazlett@apple.com> Co-authored-by: Gilbert Song <gilbertsong@apple.com> Co-authored-by: Hugh Bussell <hbussell@apple.com> Co-authored-by: John Logan <john_logan@apple.com> Co-authored-by: Kathryn Baldauf <k_baldauf@apple.com> Co-authored-by: Madhu Venugopal <mvenugopal@apple.com> Co-authored-by: Michael Crosby <michael_crosby@apple.com> Co-authored-by: Sidhartha Mani <sidhartha_mani@apple.com> Co-authored-by: Tanweer Noor <tnoor@apple.com> Co-authored-by: Ximena Perez Diaz <xperez528@gmail.com> Co-authored-by: Yibo Zhuang <yzhuang@apple.com>
252 lines
8.3 KiB
Swift
252 lines
8.3 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import ContainerNetworkService
|
|
import ContainerXPC
|
|
import Containerization
|
|
import ContainerizationError
|
|
import ContainerizationOCI
|
|
import Foundation
|
|
import TerminalProgress
|
|
|
|
public struct ClientContainer: Sendable, Codable {
|
|
static let serviceIdentifier = "com.apple.container.apiserver"
|
|
|
|
private var sandboxClient: SandboxClient {
|
|
SandboxClient(id: configuration.id, runtime: configuration.runtimeHandler)
|
|
}
|
|
|
|
/// Identifier of the container.
|
|
public var id: String {
|
|
configuration.id
|
|
}
|
|
|
|
public let status: RuntimeStatus
|
|
|
|
/// Configured platform for the container.
|
|
public var platform: ContainerizationOCI.Platform {
|
|
configuration.platform
|
|
}
|
|
|
|
/// Configuration for the container.
|
|
public let configuration: ContainerConfiguration
|
|
|
|
/// Network allocated to the container.
|
|
public let networks: [Attachment]
|
|
|
|
package init(configuration: ContainerConfiguration) {
|
|
self.configuration = configuration
|
|
self.status = .stopped
|
|
self.networks = []
|
|
}
|
|
|
|
init(snapshot: ContainerSnapshot) {
|
|
self.configuration = snapshot.configuration
|
|
self.status = snapshot.status
|
|
self.networks = snapshot.networks
|
|
}
|
|
|
|
public var initProcess: ClientProcess {
|
|
ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
|
|
}
|
|
}
|
|
|
|
extension ClientContainer {
|
|
private static func newClient() -> XPCClient {
|
|
XPCClient(service: serviceIdentifier)
|
|
}
|
|
|
|
@discardableResult
|
|
private static func xpcSend(
|
|
client: XPCClient,
|
|
message: XPCMessage,
|
|
timeout: Duration? = .seconds(15)
|
|
) async throws -> XPCMessage {
|
|
try await client.send(message, responseTimeout: timeout)
|
|
}
|
|
|
|
public static func create(
|
|
configuration: ContainerConfiguration,
|
|
options: ContainerCreateOptions = .default,
|
|
kernel: Kernel
|
|
) async throws -> ClientContainer {
|
|
do {
|
|
let client = Self.newClient()
|
|
let request = XPCMessage(route: .createContainer)
|
|
|
|
let data = try JSONEncoder().encode(configuration)
|
|
let kdata = try JSONEncoder().encode(kernel)
|
|
let odata = try JSONEncoder().encode(options)
|
|
request.set(key: .containerConfig, value: data)
|
|
request.set(key: .kernel, value: kdata)
|
|
request.set(key: .containerOptions, value: odata)
|
|
|
|
try await xpcSend(client: client, message: request)
|
|
return ClientContainer(configuration: configuration)
|
|
} catch {
|
|
throw ContainerizationError(
|
|
.internalError,
|
|
message: "failed to create container",
|
|
cause: error
|
|
)
|
|
}
|
|
}
|
|
|
|
public static func list() async throws -> [ClientContainer] {
|
|
do {
|
|
let client = Self.newClient()
|
|
let request = XPCMessage(route: .listContainer)
|
|
|
|
let response = try await xpcSend(
|
|
client: client,
|
|
message: request,
|
|
timeout: .seconds(10)
|
|
)
|
|
let data = response.dataNoCopy(key: .containers)
|
|
guard let data else {
|
|
return []
|
|
}
|
|
let configs = try JSONDecoder().decode([ContainerSnapshot].self, from: data)
|
|
return configs.map { ClientContainer(snapshot: $0) }
|
|
} catch {
|
|
throw ContainerizationError(
|
|
.internalError,
|
|
message: "failed to list containers",
|
|
cause: error
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Get the container for the provided id.
|
|
public static func get(id: String) async throws -> ClientContainer {
|
|
let containers = try await list()
|
|
guard let container = containers.first(where: { $0.id == id }) else {
|
|
throw ContainerizationError(
|
|
.notFound,
|
|
message: "get failed: container \(id) not found"
|
|
)
|
|
}
|
|
return container
|
|
}
|
|
}
|
|
|
|
extension ClientContainer {
|
|
public func bootstrap() async throws -> ClientProcess {
|
|
let client = self.sandboxClient
|
|
try await client.bootstrap()
|
|
return ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
|
|
}
|
|
|
|
/// Stop the container and all processes currently executing inside.
|
|
public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
|
|
do {
|
|
let client = self.sandboxClient
|
|
try await client.stop(options: opts)
|
|
} catch {
|
|
throw ContainerizationError(
|
|
.internalError,
|
|
message: "failed to stop container",
|
|
cause: error
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Delete the container along with any resources.
|
|
public func delete() async throws {
|
|
do {
|
|
let client = XPCClient(service: Self.serviceIdentifier)
|
|
let request = XPCMessage(route: .deleteContainer)
|
|
request.set(key: .id, value: self.id)
|
|
try await client.send(request)
|
|
} catch {
|
|
throw ContainerizationError(
|
|
.internalError,
|
|
message: "failed to delete container",
|
|
cause: error
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ClientContainer {
|
|
/// Execute a new process inside a running container.
|
|
public func createProcess(id: String, configuration: ProcessConfiguration) async throws -> ClientProcess {
|
|
do {
|
|
let client = self.sandboxClient
|
|
try await client.createProcess(id, config: configuration)
|
|
return ClientProcessImpl(containerId: self.id, processId: id, client: client)
|
|
} catch {
|
|
throw ContainerizationError(
|
|
.internalError,
|
|
message: "failed to exec in container",
|
|
cause: error
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Send or "kill" a signal to the initial process of the container.
|
|
/// Kill does not wait for the process to exit, it only delivers the signal.
|
|
public func kill(_ signal: Int32) async throws {
|
|
do {
|
|
let client = self.sandboxClient
|
|
try await client.kill(self.id, signal: Int64(signal))
|
|
} catch {
|
|
throw ContainerizationError(
|
|
.internalError,
|
|
message: "failed to kill container \(self.id)",
|
|
cause: error
|
|
)
|
|
}
|
|
}
|
|
|
|
public func logs() async throws -> [FileHandle] {
|
|
do {
|
|
let client = XPCClient(service: Self.serviceIdentifier)
|
|
let request = XPCMessage(route: .containerLogs)
|
|
request.set(key: .id, value: self.id)
|
|
|
|
let response = try await client.send(request)
|
|
let fds = response.fileHandles(key: .logs)
|
|
guard let fds else {
|
|
throw ContainerizationError(
|
|
.internalError,
|
|
message: "No log fds returned"
|
|
)
|
|
}
|
|
return fds
|
|
} catch {
|
|
throw ContainerizationError(
|
|
.internalError,
|
|
message: "failed to get logs for container \(self.id)",
|
|
cause: error
|
|
)
|
|
}
|
|
}
|
|
|
|
public func dial(_ port: UInt32) async throws -> FileHandle {
|
|
do {
|
|
let client = self.sandboxClient
|
|
return try await client.dial(port)
|
|
} catch {
|
|
throw ContainerizationError(
|
|
.internalError,
|
|
message: "failed to dial \(port) in container \(self.id)",
|
|
cause: error
|
|
)
|
|
}
|
|
}
|
|
}
|