mirror of
https://github.com/apple/container.git
synced 2026-08-24 10:05:43 -05:00
CLI: Rework ClientContainer (#1139)
ClientContainer was honestly extremely awkward. It could only be created by passing either a ContainerConfiguration, or a Snapshot that had to be obtained from calling a static method on the type itself. The type also did not store a connection, so every single method got a new xpc connection to the APIServer. This change aims to rework this type to be just a generic client, that is *not* a client for one specific container, but for any. - Rename to ContainerClient - Have list() return [ContainerSnapshot] - Create a connection in the constructor - Change all the callsites to use the new API - Small, somewhat related, change to logs API in the APIServer. Now that we don't need to call get() to grab a client anymore which was typically what did "does this container exist" logic and gave a nice error message, I added a small check in the APIServer to see if the container exists and return mostly the same error message.
This commit is contained in:
@@ -153,10 +153,10 @@ extension Application {
|
||||
}
|
||||
|
||||
group.addTask { [vsockPort, cpus, memory, log, dnsNameservers] in
|
||||
let client = ContainerClient()
|
||||
while true {
|
||||
do {
|
||||
let container = try await ClientContainer.get(id: "buildkit")
|
||||
let fh = try await container.dial(vsockPort)
|
||||
let fh = try await client.dial(id: "buildkit", port: vsockPort)
|
||||
|
||||
let threadGroup: MultiThreadedEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
|
||||
let b = try Builder(socket: fh, group: threadGroup)
|
||||
|
||||
@@ -39,14 +39,15 @@ extension Application {
|
||||
|
||||
public func run() async throws {
|
||||
do {
|
||||
let container = try await ClientContainer.get(id: "buildkit")
|
||||
let client = ContainerClient()
|
||||
let container = try await client.get(id: "buildkit")
|
||||
if container.status != .stopped {
|
||||
guard force else {
|
||||
throw ContainerizationError(.invalidState, message: "BuildKit container is not stopped, use --force to override")
|
||||
}
|
||||
try await container.stop()
|
||||
try await client.stop(id: container.id)
|
||||
}
|
||||
try await container.delete()
|
||||
try await client.delete(id: container.id)
|
||||
} catch {
|
||||
if error is ContainerizationError {
|
||||
if (error as? ContainerizationError)?.code == .notFound {
|
||||
|
||||
@@ -119,7 +119,8 @@ extension Application {
|
||||
}
|
||||
targetEnvVars.sort()
|
||||
|
||||
let existingContainer = try? await ClientContainer.get(id: "buildkit")
|
||||
let client = ContainerClient()
|
||||
let existingContainer = try? await client.get(id: "buildkit")
|
||||
if let existingContainer {
|
||||
let existingImage = existingContainer.configuration.image.reference
|
||||
let existingResources = existingContainer.configuration.resources
|
||||
@@ -174,16 +175,16 @@ extension Application {
|
||||
return
|
||||
}
|
||||
// If they changed, stop and delete the existing builder
|
||||
try await existingContainer.stop()
|
||||
try await existingContainer.delete()
|
||||
try await client.stop(id: existingContainer.id)
|
||||
try await client.delete(id: existingContainer.id)
|
||||
case .stopped:
|
||||
// If the builder is stopped and matches our requirements, start it
|
||||
// Otherwise, delete it and create a new one
|
||||
guard imageChanged || cpuChanged || memChanged || envChanged || dnsChanged else {
|
||||
try await existingContainer.startBuildKit(progressUpdate, nil)
|
||||
try await startBuildKit(client: client, id: existingContainer.id, progressUpdate, nil)
|
||||
return
|
||||
}
|
||||
try await existingContainer.delete()
|
||||
try await client.delete(id: existingContainer.id)
|
||||
case .stopping:
|
||||
throw ContainerizationError(
|
||||
.invalidState,
|
||||
@@ -296,43 +297,46 @@ extension Application {
|
||||
.setDescription("Starting BuildKit container")
|
||||
])
|
||||
|
||||
let container = try await ClientContainer.create(
|
||||
try await client.create(
|
||||
configuration: config,
|
||||
options: .default,
|
||||
kernel: kernel
|
||||
)
|
||||
|
||||
try await container.startBuildKit(progressUpdate, taskManager)
|
||||
try await startBuildKit(client: client, id: Builder.builderContainerId, progressUpdate, taskManager)
|
||||
log.debug("starting BuildKit and BuildKit-shim")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ClientContainer Extension for BuildKit
|
||||
// MARK: - BuildKit Start Helper
|
||||
|
||||
extension ClientContainer {
|
||||
/// Starts the BuildKit process within the container
|
||||
/// This method handles bootstrapping the container and starting the BuildKit process
|
||||
fileprivate func startBuildKit(_ progress: @escaping ProgressUpdateHandler, _ taskManager: ProgressTaskCoordinator? = nil) async throws {
|
||||
do {
|
||||
let io = try ProcessIO.create(
|
||||
tty: false,
|
||||
interactive: false,
|
||||
detach: true
|
||||
)
|
||||
defer { try? io.close() }
|
||||
/// Starts the BuildKit process within the container
|
||||
/// This function handles bootstrapping the container and starting the BuildKit process
|
||||
private func startBuildKit(
|
||||
client: ContainerClient,
|
||||
id: String,
|
||||
_ progress: @escaping ProgressUpdateHandler,
|
||||
_ taskManager: ProgressTaskCoordinator? = nil
|
||||
) async throws {
|
||||
do {
|
||||
let io = try ProcessIO.create(
|
||||
tty: false,
|
||||
interactive: false,
|
||||
detach: true
|
||||
)
|
||||
defer { try? io.close() }
|
||||
|
||||
let process = try await bootstrap(stdio: io.stdio)
|
||||
try await process.start()
|
||||
await taskManager?.finish()
|
||||
try io.closeAfterStart()
|
||||
} catch {
|
||||
try? await stop()
|
||||
try? await delete()
|
||||
if error is ContainerizationError {
|
||||
throw error
|
||||
}
|
||||
throw ContainerizationError(.internalError, message: "failed to start BuildKit: \(error)")
|
||||
let process = try await client.bootstrap(id: id, stdio: io.stdio)
|
||||
try await process.start()
|
||||
await taskManager?.finish()
|
||||
try io.closeAfterStart()
|
||||
} catch {
|
||||
try? await client.stop(id: id)
|
||||
try? await client.delete(id: id)
|
||||
if error is ContainerizationError {
|
||||
throw error
|
||||
}
|
||||
throw ContainerizationError(.internalError, message: "failed to start BuildKit: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerResource
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import Foundation
|
||||
@@ -42,7 +43,8 @@ extension Application {
|
||||
|
||||
public func run() async throws {
|
||||
do {
|
||||
let container = try await ClientContainer.get(id: "buildkit")
|
||||
let client = ContainerClient()
|
||||
let container = try await client.get(id: "buildkit")
|
||||
try printContainers(containers: [container], format: format)
|
||||
} catch {
|
||||
if error is ContainerizationError {
|
||||
@@ -59,7 +61,7 @@ extension Application {
|
||||
[["ID", "IMAGE", "STATE", "ADDR", "CPUS", "MEMORY"]]
|
||||
}
|
||||
|
||||
private func printContainers(containers: [ClientContainer], format: ListFormat) throws {
|
||||
private func printContainers(containers: [ContainerSnapshot], format: ListFormat) throws {
|
||||
if format == .json {
|
||||
let printables = containers.map {
|
||||
PrintableContainer($0)
|
||||
@@ -88,7 +90,7 @@ extension Application {
|
||||
}
|
||||
}
|
||||
|
||||
extension ClientContainer {
|
||||
extension ContainerSnapshot {
|
||||
fileprivate var asRow: [String] {
|
||||
[
|
||||
self.id,
|
||||
|
||||
@@ -35,8 +35,8 @@ extension Application {
|
||||
|
||||
public func run() async throws {
|
||||
do {
|
||||
let container = try await ClientContainer.get(id: "buildkit")
|
||||
try await container.stop()
|
||||
let client = ContainerClient()
|
||||
try await client.stop(id: "buildkit")
|
||||
} catch {
|
||||
if error is ContainerizationError {
|
||||
if (error as? ContainerizationError)?.code == .notFound {
|
||||
|
||||
@@ -82,11 +82,12 @@ extension Application {
|
||||
)
|
||||
|
||||
let options = ContainerCreateOptions(autoRemove: managementFlags.remove)
|
||||
let container = try await ClientContainer.create(configuration: ck.0, options: options, kernel: ck.1)
|
||||
let client = ContainerClient()
|
||||
try await client.create(configuration: ck.0, options: options, kernel: ck.1)
|
||||
|
||||
if !self.managementFlags.cidfile.isEmpty {
|
||||
let path = self.managementFlags.cidfile
|
||||
let data = container.id.data(using: .utf8)
|
||||
let data = id.data(using: .utf8)
|
||||
var attributes = [FileAttributeKey: Any]()
|
||||
attributes[.posixPermissions] = 0o644
|
||||
let success = FileManager.default.createFile(
|
||||
@@ -101,7 +102,7 @@ extension Application {
|
||||
}
|
||||
progress.finish()
|
||||
|
||||
print(container.id)
|
||||
print(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerResource
|
||||
import ContainerizationError
|
||||
import Foundation
|
||||
|
||||
@@ -54,12 +55,13 @@ extension Application {
|
||||
|
||||
public mutating func run() async throws {
|
||||
let set = Set<String>(containerIds)
|
||||
var containers = [ClientContainer]()
|
||||
let client = ContainerClient()
|
||||
var containers = [ContainerSnapshot]()
|
||||
|
||||
if all {
|
||||
containers = try await ClientContainer.list()
|
||||
containers = try await client.list()
|
||||
} else {
|
||||
let ctrs = try await ClientContainer.list()
|
||||
let ctrs = try await client.list()
|
||||
containers = ctrs.filter { c in
|
||||
set.contains(c.id)
|
||||
}
|
||||
@@ -94,7 +96,7 @@ extension Application {
|
||||
return nil // Skip running container when using --all
|
||||
}
|
||||
|
||||
try await container.delete(force: force)
|
||||
try await client.delete(id: container.id, force: force)
|
||||
print(container.id)
|
||||
return nil
|
||||
} catch {
|
||||
|
||||
@@ -45,7 +45,8 @@ extension Application {
|
||||
|
||||
public func run() async throws {
|
||||
var exitCode: Int32 = 127
|
||||
let container = try await ClientContainer.get(id: containerId)
|
||||
let client = ContainerClient()
|
||||
let container = try await client.get(id: containerId)
|
||||
try ensureRunning(container: container)
|
||||
|
||||
let stdin = self.processFlags.interactive
|
||||
@@ -79,8 +80,9 @@ extension Application {
|
||||
try? io.close()
|
||||
}
|
||||
|
||||
let process = try await container.createProcess(
|
||||
id: UUID().uuidString.lowercased(),
|
||||
let process = try await client.createProcess(
|
||||
containerId: container.id,
|
||||
processId: UUID().uuidString.lowercased(),
|
||||
configuration: config,
|
||||
stdio: io.stdio
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerResource
|
||||
import Foundation
|
||||
import SwiftProtobuf
|
||||
|
||||
@@ -34,7 +35,8 @@ extension Application {
|
||||
var containerIds: [String]
|
||||
|
||||
public func run() async throws {
|
||||
let objects: [any Codable] = try await ClientContainer.list().filter {
|
||||
let client = ContainerClient()
|
||||
let objects: [any Codable] = try await client.list().filter {
|
||||
containerIds.contains($0.id)
|
||||
}.map {
|
||||
PrintableContainer($0)
|
||||
|
||||
@@ -51,8 +51,9 @@ extension Application {
|
||||
|
||||
public mutating func run() async throws {
|
||||
let set = Set<String>(containerIds)
|
||||
let client = ContainerClient()
|
||||
|
||||
var containers = try await ClientContainer.list().filter { c in
|
||||
var containers = try await client.list().filter { c in
|
||||
c.status == .running
|
||||
}
|
||||
if !self.all {
|
||||
@@ -66,7 +67,7 @@ extension Application {
|
||||
var failed: [String] = []
|
||||
for container in containers {
|
||||
do {
|
||||
try await container.kill(signalNumber)
|
||||
try await client.kill(id: container.id, signal: signalNumber)
|
||||
print(container.id)
|
||||
} catch {
|
||||
log.error("failed to kill container \(container.id): \(error)")
|
||||
|
||||
@@ -43,7 +43,8 @@ extension Application {
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
let containers = try await ClientContainer.list()
|
||||
let client = ContainerClient()
|
||||
let containers = try await client.list()
|
||||
try printContainers(containers: containers, format: format)
|
||||
}
|
||||
|
||||
@@ -51,7 +52,7 @@ extension Application {
|
||||
[["ID", "IMAGE", "OS", "ARCH", "STATE", "ADDR", "CPUS", "MEMORY", "STARTED"]]
|
||||
}
|
||||
|
||||
private func printContainers(containers: [ClientContainer], format: ListFormat) throws {
|
||||
private func printContainers(containers: [ContainerSnapshot], format: ListFormat) throws {
|
||||
if format == .json {
|
||||
let printables = containers.map {
|
||||
PrintableContainer($0)
|
||||
@@ -86,13 +87,13 @@ extension Application {
|
||||
}
|
||||
}
|
||||
|
||||
extension ClientContainer {
|
||||
extension ContainerSnapshot {
|
||||
fileprivate var asRow: [String] {
|
||||
[
|
||||
self.id,
|
||||
self.configuration.image.reference,
|
||||
self.configuration.platform.os,
|
||||
self.configuration.platform.architecture,
|
||||
self.platform.os,
|
||||
self.platform.architecture,
|
||||
self.status.rawValue,
|
||||
self.networks.compactMap { $0.ipv4Address.description }.joined(separator: ","),
|
||||
"\(self.configuration.resources.cpus)",
|
||||
@@ -108,7 +109,7 @@ struct PrintableContainer: Codable {
|
||||
let networks: [Attachment]
|
||||
let startedDate: Date?
|
||||
|
||||
init(_ container: ClientContainer) {
|
||||
init(_ container: ContainerSnapshot) {
|
||||
self.status = container.status
|
||||
self.configuration = container.configuration
|
||||
self.networks = container.networks
|
||||
|
||||
@@ -46,22 +46,15 @@ extension Application {
|
||||
var containerId: String
|
||||
|
||||
public func run() async throws {
|
||||
do {
|
||||
let container = try await ClientContainer.get(id: containerId)
|
||||
let fhs = try await container.logs()
|
||||
let fileHandle = boot ? fhs[1] : fhs[0]
|
||||
let client = ContainerClient()
|
||||
let fhs = try await client.logs(id: containerId)
|
||||
let fileHandle = boot ? fhs[1] : fhs[0]
|
||||
|
||||
try await Self.tail(
|
||||
fh: fileHandle,
|
||||
n: numLines,
|
||||
follow: follow
|
||||
)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.invalidArgument,
|
||||
message: "failed to fetch container logs for \(containerId): \(error)"
|
||||
)
|
||||
}
|
||||
try await Self.tail(
|
||||
fh: fileHandle,
|
||||
n: numLines,
|
||||
follow: follow
|
||||
)
|
||||
}
|
||||
|
||||
private static func tail(
|
||||
|
||||
@@ -32,16 +32,17 @@ extension Application {
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
public func run() async throws {
|
||||
let containersToPrune = try await ClientContainer.list().filter { $0.status == .stopped }
|
||||
let client = ContainerClient()
|
||||
let containersToPrune = try await client.list().filter { $0.status == .stopped }
|
||||
|
||||
var prunedContainerIds = [String]()
|
||||
var totalSize: UInt64 = 0
|
||||
|
||||
for container in containersToPrune {
|
||||
do {
|
||||
let actualSize = try await ClientContainer.containerDiskUsage(id: container.id)
|
||||
let actualSize = try await client.diskUsage(id: container.id)
|
||||
totalSize += actualSize
|
||||
try await container.delete()
|
||||
try await client.delete(id: container.id)
|
||||
prunedContainerIds.append(container.id)
|
||||
} catch {
|
||||
log.error("Failed to prune container \(container.id): \(error)")
|
||||
|
||||
@@ -85,7 +85,8 @@ extension Application {
|
||||
try Utility.validEntityName(id)
|
||||
|
||||
// Check if container with id already exists.
|
||||
let existing = try? await ClientContainer.get(id: id)
|
||||
let client = ContainerClient()
|
||||
let existing = try? await client.get(id: id)
|
||||
guard existing == nil else {
|
||||
throw ContainerizationError(
|
||||
.exists,
|
||||
@@ -108,7 +109,7 @@ extension Application {
|
||||
progress.set(description: "Starting container")
|
||||
|
||||
let options = ContainerCreateOptions(autoRemove: managementFlags.remove)
|
||||
let container = try await ClientContainer.create(
|
||||
try await client.create(
|
||||
configuration: ck.0,
|
||||
options: options,
|
||||
kernel: ck.1
|
||||
@@ -125,7 +126,7 @@ extension Application {
|
||||
try? io.close()
|
||||
}
|
||||
|
||||
let process = try await container.bootstrap(stdio: io.stdio)
|
||||
let process = try await client.bootstrap(id: id, stdio: io.stdio)
|
||||
progress.finish()
|
||||
|
||||
if !self.managementFlags.cidfile.isEmpty {
|
||||
@@ -161,7 +162,7 @@ extension Application {
|
||||
|
||||
exitCode = try await io.handleProcess(process: process, log: log)
|
||||
} catch {
|
||||
try? await container.delete()
|
||||
try? await client.delete(id: id)
|
||||
if error is ContainerizationError {
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -54,7 +54,8 @@ extension Application {
|
||||
progress.start()
|
||||
|
||||
let detach = !self.attach && !self.interactive
|
||||
let container = try await ClientContainer.get(id: containerId)
|
||||
let client = ContainerClient()
|
||||
let container = try await client.get(id: containerId)
|
||||
|
||||
// Bootstrap and process start are both idempotent and don't fail the second time
|
||||
// around, however not doing an rpc is always faster :). The other bit is we don't
|
||||
@@ -86,7 +87,7 @@ extension Application {
|
||||
try? io.close()
|
||||
}
|
||||
|
||||
let process = try await container.bootstrap(stdio: io.stdio)
|
||||
let process = try await client.bootstrap(id: container.id, stdio: io.stdio)
|
||||
progress.finish()
|
||||
|
||||
if detach {
|
||||
@@ -98,7 +99,7 @@ extension Application {
|
||||
|
||||
exitCode = try await io.handleProcess(process: process, log: log)
|
||||
} catch {
|
||||
try? await container.stop()
|
||||
try? await client.stop(id: container.id)
|
||||
|
||||
if error is ContainerizationError {
|
||||
throw error
|
||||
|
||||
@@ -62,15 +62,16 @@ extension Application {
|
||||
}
|
||||
|
||||
private func runStatic() async throws {
|
||||
let allContainers = try await ClientContainer.list()
|
||||
let client = ContainerClient()
|
||||
let allContainers = try await client.list()
|
||||
|
||||
let containersToShow: [ClientContainer]
|
||||
let containersToShow: [ContainerSnapshot]
|
||||
if containers.isEmpty {
|
||||
// No containers specified - show all running containers
|
||||
containersToShow = allContainers.filter { $0.status == .running }
|
||||
} else {
|
||||
// Validate all specified containers exist before proceeding
|
||||
var found: [ClientContainer] = []
|
||||
var found: [ContainerSnapshot] = []
|
||||
for containerId in containers {
|
||||
guard let container = allContainers.first(where: { $0.id == containerId || $0.id.starts(with: containerId) }) else {
|
||||
throw ContainerizationError(
|
||||
@@ -83,7 +84,7 @@ extension Application {
|
||||
containersToShow = found
|
||||
}
|
||||
|
||||
let statsData = try await collectStats(for: containersToShow)
|
||||
let statsData = try await collectStats(client: client, for: containersToShow)
|
||||
|
||||
if format == .json {
|
||||
let jsonStats = statsData.map { $0.stats2 }
|
||||
@@ -96,9 +97,11 @@ extension Application {
|
||||
}
|
||||
|
||||
private func runStreaming() async throws {
|
||||
let client = ContainerClient()
|
||||
|
||||
// If containers were specified, validate they all exist upfront
|
||||
if !containers.isEmpty {
|
||||
let allContainers = try await ClientContainer.list()
|
||||
let allContainers = try await client.list()
|
||||
for containerId in containers {
|
||||
guard allContainers.first(where: { $0.id == containerId || $0.id.starts(with: containerId) }) != nil else {
|
||||
throw ContainerizationError(
|
||||
@@ -115,13 +118,13 @@ extension Application {
|
||||
|
||||
while true {
|
||||
do {
|
||||
let allContainers = try await ClientContainer.list()
|
||||
let allContainers = try await client.list()
|
||||
|
||||
let containersToShow: [ClientContainer]
|
||||
let containersToShow: [ContainerSnapshot]
|
||||
if containers.isEmpty {
|
||||
containersToShow = allContainers.filter { $0.status == .running }
|
||||
} else {
|
||||
var found: [ClientContainer] = []
|
||||
var found: [ContainerSnapshot] = []
|
||||
for containerId in containers {
|
||||
if let container = allContainers.first(where: { $0.id == containerId || $0.id.starts(with: containerId) }) {
|
||||
found.append(container)
|
||||
@@ -130,7 +133,7 @@ extension Application {
|
||||
containersToShow = found
|
||||
}
|
||||
|
||||
let statsData = try await collectStats(for: containersToShow)
|
||||
let statsData = try await collectStats(client: client, for: containersToShow)
|
||||
|
||||
// Clear screen and reprint
|
||||
clearScreen()
|
||||
@@ -148,19 +151,19 @@ extension Application {
|
||||
}
|
||||
|
||||
private struct StatsSnapshot {
|
||||
let container: ClientContainer
|
||||
let container: ContainerSnapshot
|
||||
let stats1: ContainerResource.ContainerStats
|
||||
let stats2: ContainerResource.ContainerStats
|
||||
}
|
||||
|
||||
private func collectStats(for containers: [ClientContainer]) async throws -> [StatsSnapshot] {
|
||||
private func collectStats(client: ContainerClient, for containers: [ContainerSnapshot]) async throws -> [StatsSnapshot] {
|
||||
var snapshots: [StatsSnapshot] = []
|
||||
|
||||
// First sample
|
||||
for container in containers {
|
||||
guard container.status == .running else { continue }
|
||||
do {
|
||||
let stats1 = try await container.stats()
|
||||
let stats1 = try await client.stats(id: container.id)
|
||||
snapshots.append(StatsSnapshot(container: container, stats1: stats1, stats2: stats1))
|
||||
} catch {
|
||||
// Skip containers that error out
|
||||
@@ -175,7 +178,7 @@ extension Application {
|
||||
// Second sample
|
||||
for i in 0..<snapshots.count {
|
||||
do {
|
||||
let stats2 = try await snapshots[i].container.stats()
|
||||
let stats2 = try await client.stats(id: snapshots[i].container.id)
|
||||
snapshots[i] = StatsSnapshot(
|
||||
container: snapshots[i].container,
|
||||
stats1: snapshots[i].stats1,
|
||||
|
||||
@@ -57,11 +57,12 @@ extension Application {
|
||||
|
||||
public mutating func run() async throws {
|
||||
let set = Set<String>(containerIds)
|
||||
var containers = [ClientContainer]()
|
||||
let client = ContainerClient()
|
||||
var containers = [ContainerSnapshot]()
|
||||
if self.all {
|
||||
containers = try await ClientContainer.list()
|
||||
containers = try await client.list()
|
||||
} else {
|
||||
containers = try await ClientContainer.list().filter { c in
|
||||
containers = try await client.list().filter { c in
|
||||
set.contains(c.id)
|
||||
}
|
||||
}
|
||||
@@ -70,7 +71,7 @@ extension Application {
|
||||
timeoutInSeconds: self.time,
|
||||
signal: try Signals.parseSignal(self.signal)
|
||||
)
|
||||
let failed = try await Self.stopContainers(containers: containers, stopOptions: opts, log: log)
|
||||
let failed = try await Self.stopContainers(client: client, containers: containers, stopOptions: opts, log: log)
|
||||
if failed.count > 0 {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
@@ -79,13 +80,13 @@ extension Application {
|
||||
}
|
||||
}
|
||||
|
||||
static func stopContainers(containers: [ClientContainer], stopOptions: ContainerStopOptions, log: Logger) async throws -> [String] {
|
||||
static func stopContainers(client: ContainerClient, containers: [ContainerSnapshot], stopOptions: ContainerStopOptions, log: Logger) async throws -> [String] {
|
||||
var failed: [String] = []
|
||||
try await withThrowingTaskGroup(of: ClientContainer?.self) { group in
|
||||
try await withThrowingTaskGroup(of: ContainerSnapshot?.self) { group in
|
||||
for container in containers {
|
||||
group.addTask {
|
||||
do {
|
||||
try await container.stop(opts: stopOptions)
|
||||
try await client.stop(id: container.id, opts: stopOptions)
|
||||
print(container.id)
|
||||
return nil
|
||||
} catch {
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerAPIClient
|
||||
import ContainerResource
|
||||
import Containerization
|
||||
import ContainerizationError
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
|
||||
extension Application {
|
||||
static func ensureRunning(container: ClientContainer) throws {
|
||||
static func ensureRunning(container: ContainerSnapshot) throws {
|
||||
if container.status != .running {
|
||||
throw ContainerizationError(.invalidState, message: "container \(container.id) is not running")
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ extension Application {
|
||||
let imagesToPrune: [ClientImage]
|
||||
if all {
|
||||
// Find all images not used by any container
|
||||
let containers = try await ClientContainer.list()
|
||||
let client = ContainerClient()
|
||||
let containers = try await client.list()
|
||||
var imagesInUse = Set<String>()
|
||||
for container in containers {
|
||||
imagesInUse.insert(container.configuration.image.reference)
|
||||
|
||||
@@ -30,7 +30,8 @@ extension Application.NetworkCommand {
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
public func run() async throws {
|
||||
let allContainers = try await ClientContainer.list()
|
||||
let client = ContainerClient()
|
||||
let allContainers = try await client.list()
|
||||
let allNetworks = try await ClientNetwork.list()
|
||||
|
||||
var networksInUse = Set<String>()
|
||||
|
||||
@@ -61,12 +61,13 @@ extension Application {
|
||||
}
|
||||
|
||||
if running {
|
||||
let client = ContainerClient()
|
||||
log.info("stopping containers", metadata: ["stopTimeoutSeconds": "\(Self.stopTimeoutSeconds)"])
|
||||
do {
|
||||
let containers = try await ClientContainer.list()
|
||||
let containers = try await client.list()
|
||||
let signal = try Signals.parseSignal("SIGTERM")
|
||||
let opts = ContainerStopOptions(timeoutInSeconds: Self.stopTimeoutSeconds, signal: signal)
|
||||
let failed = try await ContainerStop.stopContainers(containers: containers, stopOptions: opts, log: log)
|
||||
let failed = try await ContainerStop.stopContainers(client: client, containers: containers, stopOptions: opts, log: log)
|
||||
if !failed.isEmpty {
|
||||
log.warning("some containers could not be stopped gracefully", metadata: ["ids": "\(failed)"])
|
||||
}
|
||||
@@ -77,7 +78,7 @@ extension Application {
|
||||
log.info("waiting for containers to exit")
|
||||
do {
|
||||
for _ in 0..<Self.shutdownTimeoutSeconds {
|
||||
let anyRunning = try await ClientContainer.list()
|
||||
let anyRunning = try await client.list()
|
||||
.contains { $0.status == .running }
|
||||
guard anyRunning else {
|
||||
break
|
||||
|
||||
@@ -32,7 +32,8 @@ extension Application.VolumeCommand {
|
||||
let allVolumes = try await ClientVolume.list()
|
||||
|
||||
// Find all volumes not used by any container
|
||||
let containers = try await ClientContainer.list()
|
||||
let client = ContainerClient()
|
||||
let containers = try await client.list()
|
||||
var volumesInUse = Set<String>()
|
||||
for container in containers {
|
||||
for mount in container.configuration.mounts {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationOCI
|
||||
import Foundation
|
||||
|
||||
/// A snapshot of a container along with its configuration
|
||||
@@ -21,6 +22,17 @@ import Foundation
|
||||
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.
|
||||
|
||||
+63
-101
@@ -20,68 +20,37 @@ import Containerization
|
||||
import ContainerizationError
|
||||
import ContainerizationOCI
|
||||
import Foundation
|
||||
import TerminalProgress
|
||||
|
||||
public struct ClientContainer: Sendable, Codable {
|
||||
static let serviceIdentifier = "com.apple.container.apiserver"
|
||||
/// A client for interacting with the container API server.
|
||||
///
|
||||
/// This client holds a reusable XPC connection and provides methods for
|
||||
/// container lifecycle operations. All methods that operate on a specific
|
||||
/// container take an `id` parameter.
|
||||
public struct ContainerClient: Sendable {
|
||||
private static let serviceIdentifier = "com.apple.container.apiserver"
|
||||
|
||||
/// Identifier of the container.
|
||||
public var id: String {
|
||||
configuration.id
|
||||
}
|
||||
private let xpcClient: XPCClient
|
||||
|
||||
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]
|
||||
|
||||
/// When the container was started.
|
||||
public let startedDate: Date?
|
||||
|
||||
package init(configuration: ContainerConfiguration) {
|
||||
self.configuration = configuration
|
||||
self.status = .stopped
|
||||
self.networks = []
|
||||
self.startedDate = nil
|
||||
}
|
||||
|
||||
init(snapshot: ContainerSnapshot) {
|
||||
self.configuration = snapshot.configuration
|
||||
self.status = snapshot.status
|
||||
self.networks = snapshot.networks
|
||||
self.startedDate = snapshot.startedDate
|
||||
}
|
||||
}
|
||||
|
||||
extension ClientContainer {
|
||||
private static func newXPCClient() -> XPCClient {
|
||||
XPCClient(service: serviceIdentifier)
|
||||
/// Creates a new container client with a connection to the API server.
|
||||
public init() {
|
||||
self.xpcClient = XPCClient(service: Self.serviceIdentifier)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private static func xpcSend(
|
||||
client: XPCClient,
|
||||
private func xpcSend(
|
||||
message: XPCMessage,
|
||||
timeout: Duration? = .seconds(15)
|
||||
) async throws -> XPCMessage {
|
||||
try await client.send(message, responseTimeout: timeout)
|
||||
try await xpcClient.send(message, responseTimeout: timeout)
|
||||
}
|
||||
|
||||
public static func create(
|
||||
/// Create a new container with the given configuration.
|
||||
public func create(
|
||||
configuration: ContainerConfiguration,
|
||||
options: ContainerCreateOptions = .default,
|
||||
kernel: Kernel
|
||||
) async throws -> ClientContainer {
|
||||
) async throws {
|
||||
do {
|
||||
let client = Self.newXPCClient()
|
||||
let request = XPCMessage(route: .containerCreate)
|
||||
|
||||
let data = try JSONEncoder().encode(configuration)
|
||||
@@ -91,8 +60,7 @@ extension ClientContainer {
|
||||
request.set(key: .kernel, value: kdata)
|
||||
request.set(key: .containerOptions, value: odata)
|
||||
|
||||
try await xpcSend(client: client, message: request)
|
||||
return ClientContainer(configuration: configuration)
|
||||
try await xpcSend(message: request)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
@@ -102,13 +70,12 @@ extension ClientContainer {
|
||||
}
|
||||
}
|
||||
|
||||
public static func list() async throws -> [ClientContainer] {
|
||||
/// List all containers.
|
||||
public func list() async throws -> [ContainerSnapshot] {
|
||||
do {
|
||||
let client = Self.newXPCClient()
|
||||
let request = XPCMessage(route: .containerList)
|
||||
|
||||
let response = try await xpcSend(
|
||||
client: client,
|
||||
message: request,
|
||||
timeout: .seconds(10)
|
||||
)
|
||||
@@ -116,8 +83,7 @@ extension ClientContainer {
|
||||
guard let data else {
|
||||
return []
|
||||
}
|
||||
let configs = try JSONDecoder().decode([ContainerSnapshot].self, from: data)
|
||||
return configs.map { ClientContainer(snapshot: $0) }
|
||||
return try JSONDecoder().decode([ContainerSnapshot].self, from: data)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
@@ -128,9 +94,9 @@ extension ClientContainer {
|
||||
}
|
||||
|
||||
/// Get the container for the provided id.
|
||||
public static func get(id: String) async throws -> ClientContainer {
|
||||
public func get(id: String) async throws -> ContainerSnapshot {
|
||||
let containers = try await list()
|
||||
guard let container = containers.first(where: { $0.id == id }) else {
|
||||
guard let container = containers.first(where: { $0.configuration.id == id }) else {
|
||||
throw ContainerizationError(
|
||||
.notFound,
|
||||
message: "get failed: container \(id) not found"
|
||||
@@ -138,12 +104,10 @@ extension ClientContainer {
|
||||
}
|
||||
return container
|
||||
}
|
||||
}
|
||||
|
||||
extension ClientContainer {
|
||||
public func bootstrap(stdio: [FileHandle?]) async throws -> ClientProcess {
|
||||
/// Bootstrap the container's init process.
|
||||
public func bootstrap(id: String, stdio: [FileHandle?]) async throws -> ClientProcess {
|
||||
let request = XPCMessage(route: .containerBootstrap)
|
||||
let client = Self.newXPCClient()
|
||||
|
||||
for (i, h) in stdio.enumerated() {
|
||||
let key: XPCKeys = try {
|
||||
@@ -162,9 +126,9 @@ extension ClientContainer {
|
||||
}
|
||||
|
||||
do {
|
||||
request.set(key: .id, value: self.id)
|
||||
try await client.send(request)
|
||||
return ClientProcessImpl(containerId: self.id, xpcClient: client)
|
||||
request.set(key: .id, value: id)
|
||||
try await xpcClient.send(request)
|
||||
return ClientProcessImpl(containerId: id, xpcClient: xpcClient)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
@@ -174,15 +138,15 @@ extension ClientContainer {
|
||||
}
|
||||
}
|
||||
|
||||
public func kill(_ signal: Int32) async throws {
|
||||
/// Send a signal to the container.
|
||||
public func kill(id: String, signal: Int32) async throws {
|
||||
do {
|
||||
let request = XPCMessage(route: .containerKill)
|
||||
request.set(key: .id, value: self.id)
|
||||
request.set(key: .processIdentifier, value: self.id)
|
||||
request.set(key: .id, value: id)
|
||||
request.set(key: .processIdentifier, value: id)
|
||||
request.set(key: .signal, value: Int64(signal))
|
||||
|
||||
let client = Self.newXPCClient()
|
||||
try await client.send(request)
|
||||
try await xpcClient.send(request)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
@@ -193,15 +157,14 @@ extension ClientContainer {
|
||||
}
|
||||
|
||||
/// Stop the container and all processes currently executing inside.
|
||||
public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
|
||||
public func stop(id: String, opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
|
||||
do {
|
||||
let client = Self.newXPCClient()
|
||||
let request = XPCMessage(route: .containerStop)
|
||||
let data = try JSONEncoder().encode(opts)
|
||||
request.set(key: .id, value: self.id)
|
||||
request.set(key: .id, value: id)
|
||||
request.set(key: .stopOptions, value: data)
|
||||
|
||||
try await client.send(request)
|
||||
try await xpcClient.send(request)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
@@ -212,13 +175,12 @@ extension ClientContainer {
|
||||
}
|
||||
|
||||
/// Delete the container along with any resources.
|
||||
public func delete(force: Bool = false) async throws {
|
||||
public func delete(id: String, force: Bool = false) async throws {
|
||||
do {
|
||||
let client = Self.newXPCClient()
|
||||
let request = XPCMessage(route: .containerDelete)
|
||||
request.set(key: .id, value: self.id)
|
||||
request.set(key: .id, value: id)
|
||||
request.set(key: .forceDelete, value: force)
|
||||
try await client.send(request)
|
||||
try await xpcClient.send(request)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
@@ -228,27 +190,28 @@ extension ClientContainer {
|
||||
}
|
||||
}
|
||||
|
||||
public static func containerDiskUsage(id: String) async throws -> UInt64 {
|
||||
let client = Self.newXPCClient()
|
||||
/// Get the disk usage for a container.
|
||||
public func diskUsage(id: String) async throws -> UInt64 {
|
||||
let request = XPCMessage(route: .containerDiskUsage)
|
||||
request.set(key: .id, value: id)
|
||||
let reply = try await client.send(request)
|
||||
let reply = try await xpcClient.send(request)
|
||||
|
||||
let size = reply.uint64(key: .containerSize)
|
||||
return size
|
||||
}
|
||||
|
||||
/// Create a new process inside a running container. The process is in a
|
||||
/// created state and must still be started.
|
||||
/// Create a new process inside a running container.
|
||||
/// The process is in a created state and must still be started.
|
||||
public func createProcess(
|
||||
id: String,
|
||||
containerId: String,
|
||||
processId: String,
|
||||
configuration: ProcessConfiguration,
|
||||
stdio: [FileHandle?]
|
||||
) async throws -> ClientProcess {
|
||||
do {
|
||||
let request = XPCMessage(route: .containerCreateProcess)
|
||||
request.set(key: .id, value: self.id)
|
||||
request.set(key: .processIdentifier, value: id)
|
||||
request.set(key: .id, value: containerId)
|
||||
request.set(key: .processIdentifier, value: processId)
|
||||
|
||||
let data = try JSONEncoder().encode(configuration)
|
||||
request.set(key: .processConfig, value: data)
|
||||
@@ -269,9 +232,8 @@ extension ClientContainer {
|
||||
}
|
||||
}
|
||||
|
||||
let client = Self.newXPCClient()
|
||||
try await client.send(request)
|
||||
return ClientProcessImpl(containerId: self.id, processId: id, xpcClient: client)
|
||||
try await xpcClient.send(request)
|
||||
return ClientProcessImpl(containerId: containerId, processId: processId, xpcClient: xpcClient)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
@@ -281,13 +243,13 @@ extension ClientContainer {
|
||||
}
|
||||
}
|
||||
|
||||
public func logs() async throws -> [FileHandle] {
|
||||
/// Get the log file handles for a container.
|
||||
public func logs(id: String) async throws -> [FileHandle] {
|
||||
do {
|
||||
let client = Self.newXPCClient()
|
||||
let request = XPCMessage(route: .containerLogs)
|
||||
request.set(key: .id, value: self.id)
|
||||
request.set(key: .id, value: id)
|
||||
|
||||
let response = try await client.send(request)
|
||||
let response = try await xpcClient.send(request)
|
||||
let fds = response.fileHandles(key: .logs)
|
||||
guard let fds else {
|
||||
throw ContainerizationError(
|
||||
@@ -299,21 +261,21 @@ extension ClientContainer {
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
message: "failed to get logs for container \(self.id)",
|
||||
message: "failed to get logs for container \(id)",
|
||||
cause: error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func dial(_ port: UInt32) async throws -> FileHandle {
|
||||
/// Dial a port on the container via vsock.
|
||||
public func dial(id: String, port: UInt32) async throws -> FileHandle {
|
||||
let request = XPCMessage(route: .containerDial)
|
||||
request.set(key: .id, value: self.id)
|
||||
request.set(key: .id, value: id)
|
||||
request.set(key: .port, value: UInt64(port))
|
||||
|
||||
let client = Self.newXPCClient()
|
||||
let response: XPCMessage
|
||||
do {
|
||||
response = try await client.send(request)
|
||||
response = try await xpcClient.send(request)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
@@ -330,13 +292,13 @@ extension ClientContainer {
|
||||
return fh
|
||||
}
|
||||
|
||||
public func stats() async throws -> ContainerStats {
|
||||
/// Get resource usage statistics for a container.
|
||||
public func stats(id: String) async throws -> ContainerStats {
|
||||
let request = XPCMessage(route: .containerStats)
|
||||
request.set(key: .id, value: self.id)
|
||||
request.set(key: .id, value: id)
|
||||
|
||||
let client = Self.newXPCClient()
|
||||
do {
|
||||
let response = try await client.send(request)
|
||||
let response = try await xpcClient.send(request)
|
||||
guard let data = response.dataNoCopy(key: .statistics) else {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
@@ -347,7 +309,7 @@ extension ClientContainer {
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
message: "failed to get statistics for container \(self.id)",
|
||||
message: "failed to get statistics for container \(id)",
|
||||
cause: error
|
||||
)
|
||||
}
|
||||
@@ -446,8 +446,11 @@ public actor ContainersService {
|
||||
self.log.debug("\(#function)")
|
||||
|
||||
// Logs doesn't care if the container is running or not, just that
|
||||
// the bundle is there, and that the files actually exist.
|
||||
// the bundle is there, and that the files actually exist. We do
|
||||
// first try and get the container state so we get a nicer error message
|
||||
// (container foo not found) however.
|
||||
do {
|
||||
_ = try _getContainerState(id: id)
|
||||
let path = self.containerRoot.appendingPathComponent(id)
|
||||
let bundle = ContainerResource.Bundle(path: path)
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user