Files
container/Sources/ContainerClient/Core/ClientContainer.swift
T
Danny Canter 23c526233d ContainerService: Move force delete logic to daemon (#536)
Today force deleting (if a container is running then stop()'ing first)
is handled entirely in the cli, which is brittle. The CLI doesn't know
if the container was started with --rm so it would have to do a weird
timeout + list dance to check if the containers gone after stopping.
This change remedies this by just having the daemon take in a `force`
boolean to the delete rpc. If this is provided and the container is
running then we'll stop first, and then cleanup. We can additionally not
cleanup if --rm was provided as the daemon has the data to determine if
a container asked for autoRemove.
2025-08-26 14:23:39 -07:00

257 lines
8.4 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(stdio: [FileHandle?]) async throws -> ClientProcess {
let client = self.sandboxClient
try await client.bootstrap(stdio: stdio)
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(force: Bool = false) async throws {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let request = XPCMessage(route: .deleteContainer)
request.set(key: .id, value: self.id)
request.set(key: .forceDelete, value: force)
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,
stdio: [FileHandle?]
) async throws -> ClientProcess {
do {
let client = self.sandboxClient
try await client.createProcess(id, config: configuration, stdio: stdio)
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
)
}
}
}