diff --git a/Sources/ContainerClient/Core/ClientContainer.swift b/Sources/ContainerClient/Core/ClientContainer.swift index d2e16e3c..4df23735 100644 --- a/Sources/ContainerClient/Core/ClientContainer.swift +++ b/Sources/ContainerClient/Core/ClientContainer.swift @@ -23,7 +23,7 @@ import Foundation import TerminalProgress public struct ClientContainer: Sendable, Codable { - static let serviceIdentifier = "com.apple.container.apiserver" + public static let serviceIdentifier = "com.apple.container.apiserver" /// Identifier of the container. public var id: String { @@ -43,16 +43,46 @@ public struct ClientContainer: Sendable, Codable { /// Network allocated to the container. public let networks: [Attachment] + /// Optional XPC client for connection reuse across operations. + private let xpcClient: XPCClient? + package init(configuration: ContainerConfiguration) { self.configuration = configuration self.status = .stopped self.networks = [] + self.xpcClient = nil } - init(snapshot: ContainerSnapshot) { + public init(snapshot: ContainerSnapshot, xpcClient: XPCClient? = nil) { self.configuration = snapshot.configuration self.status = snapshot.status self.networks = snapshot.networks + self.xpcClient = xpcClient + } + + private func client() -> XPCClient { + xpcClient ?? Self.newXPCClient() + } + + enum CodingKeys: String, CodingKey { + case configuration + case status + case networks + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.configuration = try container.decode(ContainerConfiguration.self, forKey: .configuration) + self.status = try container.decode(RuntimeStatus.self, forKey: .status) + self.networks = try container.decode([Attachment].self, forKey: .networks) + self.xpcClient = nil + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(configuration, forKey: .configuration) + try container.encode(status, forKey: .status) + try container.encode(networks, forKey: .networks) } } @@ -74,7 +104,7 @@ extension ClientContainer { configuration: ContainerConfiguration, options: ContainerCreateOptions = .default, kernel: Kernel - ) async throws -> ClientContainer { + ) async throws -> ContainerSnapshot { do { let client = Self.newXPCClient() let request = XPCMessage(route: .containerCreate) @@ -87,7 +117,11 @@ extension ClientContainer { request.set(key: .containerOptions, value: odata) try await xpcSend(client: client, message: request) - return ClientContainer(configuration: configuration) + return ContainerSnapshot( + configuration: configuration, + status: .stopped, + networks: [] + ) } catch { throw ContainerizationError( .internalError, @@ -97,7 +131,7 @@ extension ClientContainer { } } - public static func list() async throws -> [ClientContainer] { + public static func list() async throws -> [ContainerSnapshot] { do { let client = Self.newXPCClient() let request = XPCMessage(route: .containerList) @@ -111,8 +145,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, @@ -123,9 +156,9 @@ extension ClientContainer { } /// Get the container for the provided id. - public static func get(id: String) async throws -> ClientContainer { + public static 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" @@ -176,7 +209,7 @@ extension ClientContainer { request.set(key: .processIdentifier, value: self.id) request.set(key: .signal, value: Int64(signal)) - let client = Self.newXPCClient() + let client = self.client() try await client.send(request) } catch { throw ContainerizationError( @@ -190,7 +223,7 @@ extension ClientContainer { /// Stop the container and all processes currently executing inside. public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws { do { - let client = Self.newXPCClient() + let client = self.client() let request = XPCMessage(route: .containerStop) let data = try JSONEncoder().encode(opts) request.set(key: .id, value: self.id) @@ -209,7 +242,7 @@ extension ClientContainer { /// Delete the container along with any resources. public func delete(force: Bool = false) async throws { do { - let client = Self.newXPCClient() + let client = self.client() let request = XPCMessage(route: .containerDelete) request.set(key: .id, value: self.id) request.set(key: .forceDelete, value: force) @@ -268,7 +301,7 @@ extension ClientContainer { public func logs() async throws -> [FileHandle] { do { - let client = Self.newXPCClient() + let client = self.client() let request = XPCMessage(route: .containerLogs) request.set(key: .id, value: self.id) @@ -295,7 +328,7 @@ extension ClientContainer { request.set(key: .id, value: self.id) request.set(key: .port, value: UInt64(port)) - let client = Self.newXPCClient() + let client = self.client() let response: XPCMessage do { response = try await client.send(request) diff --git a/Sources/ContainerCommands/BuildCommand.swift b/Sources/ContainerCommands/BuildCommand.swift index ef570f56..e7878c61 100644 --- a/Sources/ContainerCommands/BuildCommand.swift +++ b/Sources/ContainerCommands/BuildCommand.swift @@ -148,7 +148,8 @@ extension Application { group.addTask { [vsockPort, cpus, memory] in while true { do { - let container = try await ClientContainer.get(id: "buildkit") + let snapshot = try await ClientContainer.get(id: "buildkit") + let container = ClientContainer(snapshot: snapshot) let fh = try await container.dial(vsockPort) let threadGroup: MultiThreadedEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) diff --git a/Sources/ContainerCommands/Builder/BuilderDelete.swift b/Sources/ContainerCommands/Builder/BuilderDelete.swift index 24d32709..b654e78a 100644 --- a/Sources/ContainerCommands/Builder/BuilderDelete.swift +++ b/Sources/ContainerCommands/Builder/BuilderDelete.swift @@ -39,8 +39,9 @@ extension Application { public func run() async throws { do { - let container = try await ClientContainer.get(id: "buildkit") - if container.status != .stopped { + let snapshot = try await ClientContainer.get(id: "buildkit") + let container = ClientContainer(snapshot: snapshot) + if snapshot.status != .stopped { guard force else { throw ContainerizationError(.invalidState, message: "BuildKit container is not stopped, use --force to override") } diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index 805dbcfc..70d1581e 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -88,10 +88,10 @@ extension Application { let builderPlatform = ContainerizationOCI.Platform(arch: "arm64", os: "linux", variant: "v8") - let existingContainer = try? await ClientContainer.get(id: "buildkit") - if let existingContainer { - let existingImage = existingContainer.configuration.image.reference - let existingResources = existingContainer.configuration.resources + let existingSnapshot = try? await ClientContainer.get(id: "buildkit") + if let existingSnapshot { + let existingImage = existingSnapshot.configuration.image.reference + let existingResources = existingSnapshot.configuration.resources // Check if we need to recreate the builder due to different image let imageChanged = existingImage != builderImage @@ -113,7 +113,9 @@ extension Application { return false }() - switch existingContainer.status { + let existingContainer = ClientContainer(snapshot: existingSnapshot) + + switch existingSnapshot.status { case .running: guard imageChanged || cpuChanged || memChanged else { // If image, mem and cpu are the same, continue using the existing builder @@ -228,11 +230,12 @@ extension Application { .setDescription("Starting BuildKit container") ]) - let container = try await ClientContainer.create( + let snapshot = try await ClientContainer.create( configuration: config, options: .default, kernel: kernel ) + let container = ClientContainer(snapshot: snapshot) try await container.startBuildKit(progressUpdate, taskManager) } diff --git a/Sources/ContainerCommands/Builder/BuilderStatus.swift b/Sources/ContainerCommands/Builder/BuilderStatus.swift index 574eeae5..b874fe04 100644 --- a/Sources/ContainerCommands/Builder/BuilderStatus.swift +++ b/Sources/ContainerCommands/Builder/BuilderStatus.swift @@ -42,8 +42,8 @@ extension Application { public func run() async throws { do { - let container = try await ClientContainer.get(id: "buildkit") - try printContainers(containers: [container], format: format) + let snapshot = try await ClientContainer.get(id: "buildkit") + try printContainers(snapshots: [snapshot], format: format) } catch { if error is ContainerizationError { if (error as? ContainerizationError)?.code == .notFound && !quiet { @@ -59,9 +59,9 @@ extension Application { [["ID", "IMAGE", "STATE", "ADDR", "CPUS", "MEMORY"]] } - private func printContainers(containers: [ClientContainer], format: ListFormat) throws { + private func printContainers(snapshots: [ContainerSnapshot], format: ListFormat) throws { if format == .json { - let printables = containers.map { + let printables = snapshots.map { PrintableContainer($0) } let data = try JSONEncoder().encode(printables) @@ -71,15 +71,15 @@ extension Application { } if self.quiet { - containers + snapshots .filter { $0.status == .running } - .forEach { print($0.id) } + .forEach { print($0.configuration.id) } return } var rows = createHeader() - for container in containers { - rows.append(container.asRow) + for snapshot in snapshots { + rows.append(snapshot.asRow) } let formatter = TableOutput(rows: rows) @@ -88,10 +88,10 @@ extension Application { } } -extension ClientContainer { +extension ContainerSnapshot { fileprivate var asRow: [String] { [ - self.id, + self.configuration.id, self.configuration.image.reference, self.status.rawValue, self.networks.compactMap { try? CIDRAddress($0.address).address.description }.joined(separator: ","), diff --git a/Sources/ContainerCommands/Builder/BuilderStop.swift b/Sources/ContainerCommands/Builder/BuilderStop.swift index 0a230e6f..de949fb1 100644 --- a/Sources/ContainerCommands/Builder/BuilderStop.swift +++ b/Sources/ContainerCommands/Builder/BuilderStop.swift @@ -35,7 +35,8 @@ extension Application { public func run() async throws { do { - let container = try await ClientContainer.get(id: "buildkit") + let snapshot = try await ClientContainer.get(id: "buildkit") + let container = ClientContainer(snapshot: snapshot) try await container.stop() } catch { if error is ContainerizationError { diff --git a/Sources/ContainerCommands/Container/ContainerCreate.swift b/Sources/ContainerCommands/Container/ContainerCreate.swift index 5e4ecd7c..5f36ccd6 100644 --- a/Sources/ContainerCommands/Container/ContainerCreate.swift +++ b/Sources/ContainerCommands/Container/ContainerCreate.swift @@ -77,11 +77,11 @@ extension Application { ) let options = ContainerCreateOptions(autoRemove: managementFlags.remove) - let container = try await ClientContainer.create(configuration: ck.0, options: options, kernel: ck.1) + let snapshot = try await ClientContainer.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 = snapshot.configuration.id.data(using: .utf8) var attributes = [FileAttributeKey: Any]() attributes[.posixPermissions] = 0o644 let success = FileManager.default.createFile( @@ -96,7 +96,7 @@ extension Application { } progress.finish() - print(container.id) + print(snapshot.configuration.id) } } } diff --git a/Sources/ContainerCommands/Container/ContainerDelete.swift b/Sources/ContainerCommands/Container/ContainerDelete.swift index ddb02a51..ec0b9b01 100644 --- a/Sources/ContainerCommands/Container/ContainerDelete.swift +++ b/Sources/ContainerCommands/Container/ContainerDelete.swift @@ -16,6 +16,7 @@ import ArgumentParser import ContainerClient +import ContainerXPC import ContainerizationError import Foundation @@ -54,22 +55,22 @@ extension Application { public mutating func run() async throws { let set = Set(containerIds) - var containers = [ClientContainer]() + var snapshots = [ContainerSnapshot]() if all { - containers = try await ClientContainer.list() + snapshots = try await ClientContainer.list() } else { let ctrs = try await ClientContainer.list() - containers = ctrs.filter { c in - set.contains(c.id) + snapshots = ctrs.filter { c in + set.contains(c.configuration.id) } // If one of the containers requested isn't present, let's throw. We don't need to do // this for --all as --all should be perfectly usable with no containers to remove; otherwise, // it'd be quite clunky. - if containers.count != set.count { + if snapshots.count != set.count { let missing = set.filter { id in - !containers.contains { c in - c.id == id + !snapshots.contains { c in + c.configuration.id == id } } throw ContainerizationError( @@ -82,23 +83,26 @@ extension Application { var failed = [String]() let force = self.force let all = self.all + let sharedClient = XPCClient(service: ClientContainer.serviceIdentifier) + try await withThrowingTaskGroup(of: String?.self) { group in - for container in containers { + for snapshot in snapshots { group.addTask { do { - if container.status == .running && !force { + if snapshot.status == .running && !force { guard all else { throw ContainerizationError(.invalidState, message: "container is running") } return nil // Skip running container when using --all } + let container = ClientContainer(snapshot: snapshot, xpcClient: sharedClient) try await container.delete(force: force) - print(container.id) + print(snapshot.configuration.id) return nil } catch { - log.error("failed to delete container \(container.id): \(error)") - return container.id + log.error("failed to delete container \(snapshot.configuration.id): \(error)") + return snapshot.configuration.id } } } diff --git a/Sources/ContainerCommands/Container/ContainerExec.swift b/Sources/ContainerCommands/Container/ContainerExec.swift index eef07aac..9813c6b8 100644 --- a/Sources/ContainerCommands/Container/ContainerExec.swift +++ b/Sources/ContainerCommands/Container/ContainerExec.swift @@ -42,13 +42,14 @@ extension Application { public func run() async throws { var exitCode: Int32 = 127 - let container = try await ClientContainer.get(id: containerId) - try ensureRunning(container: container) + let snapshot = try await ClientContainer.get(id: containerId) + let container = ClientContainer(snapshot: snapshot) + try ensureRunning(snapshot: snapshot) let stdin = self.processFlags.interactive let tty = self.processFlags.tty - var config = container.configuration.initProcess + var config = snapshot.configuration.initProcess config.executable = arguments.first! config.arguments = [String](self.arguments.dropFirst()) config.terminal = tty diff --git a/Sources/ContainerCommands/Container/ContainerInspect.swift b/Sources/ContainerCommands/Container/ContainerInspect.swift index 26ad1033..57259bf8 100644 --- a/Sources/ContainerCommands/Container/ContainerInspect.swift +++ b/Sources/ContainerCommands/Container/ContainerInspect.swift @@ -35,7 +35,7 @@ extension Application { public func run() async throws { let objects: [any Codable] = try await ClientContainer.list().filter { - containerIds.contains($0.id) + containerIds.contains($0.configuration.id) }.map { PrintableContainer($0) } diff --git a/Sources/ContainerCommands/Container/ContainerKill.swift b/Sources/ContainerCommands/Container/ContainerKill.swift index f26d019d..afca1035 100644 --- a/Sources/ContainerCommands/Container/ContainerKill.swift +++ b/Sources/ContainerCommands/Container/ContainerKill.swift @@ -16,6 +16,7 @@ import ArgumentParser import ContainerClient +import ContainerXPC import ContainerizationError import ContainerizationOS import Darwin @@ -52,25 +53,27 @@ extension Application { public mutating func run() async throws { let set = Set(containerIds) - var containers = try await ClientContainer.list().filter { c in + var snapshots = try await ClientContainer.list().filter { c in c.status == .running } if !self.all { - containers = containers.filter { c in - set.contains(c.id) + snapshots = snapshots.filter { c in + set.contains(c.configuration.id) } } let signalNumber = try Signals.parseSignal(signal) + let sharedClient = XPCClient(service: ClientContainer.serviceIdentifier) var failed: [String] = [] - for container in containers { + for snapshot in snapshots { do { + let container = ClientContainer(snapshot: snapshot, xpcClient: sharedClient) try await container.kill(signalNumber) - print(container.id) + print(snapshot.configuration.id) } catch { - log.error("failed to kill container \(container.id): \(error)") - failed.append(container.id) + log.error("failed to kill container \(snapshot.configuration.id): \(error)") + failed.append(snapshot.configuration.id) } } if failed.count > 0 { diff --git a/Sources/ContainerCommands/Container/ContainerList.swift b/Sources/ContainerCommands/Container/ContainerList.swift index dbc3149d..cf4118b6 100644 --- a/Sources/ContainerCommands/Container/ContainerList.swift +++ b/Sources/ContainerCommands/Container/ContainerList.swift @@ -43,17 +43,17 @@ extension Application { public init() {} public func run() async throws { - let containers = try await ClientContainer.list() - try printContainers(containers: containers, format: format) + let snapshots = try await ClientContainer.list() + try printContainers(snapshots: snapshots, format: format) } private func createHeader() -> [[String]] { [["ID", "IMAGE", "OS", "ARCH", "STATE", "ADDR", "CPUS", "MEMORY"]] } - private func printContainers(containers: [ClientContainer], format: ListFormat) throws { + private func printContainers(snapshots: [ContainerSnapshot], format: ListFormat) throws { if format == .json { - let printables = containers.map { + let printables = snapshots.map { PrintableContainer($0) } let data = try JSONEncoder().encode(printables) @@ -63,21 +63,21 @@ extension Application { } if self.quiet { - containers.forEach { + snapshots.forEach { if !self.all && $0.status != .running { return } - print($0.id) + print($0.configuration.id) } return } var rows = createHeader() - for container in containers { - if !self.all && container.status != .running { + for snapshot in snapshots { + if !self.all && snapshot.status != .running { continue } - rows.append(container.asRow) + rows.append(snapshot.asRow) } let formatter = TableOutput(rows: rows) @@ -86,10 +86,10 @@ extension Application { } } -extension ClientContainer { +extension ContainerSnapshot { fileprivate var asRow: [String] { [ - self.id, + self.configuration.id, self.configuration.image.reference, self.configuration.platform.os, self.configuration.platform.architecture, @@ -106,9 +106,9 @@ struct PrintableContainer: Codable { let configuration: ContainerConfiguration let networks: [Attachment] - init(_ container: ClientContainer) { - self.status = container.status - self.configuration = container.configuration - self.networks = container.networks + init(_ snapshot: ContainerSnapshot) { + self.status = snapshot.status + self.configuration = snapshot.configuration + self.networks = snapshot.networks } } diff --git a/Sources/ContainerCommands/Container/ContainerLogs.swift b/Sources/ContainerCommands/Container/ContainerLogs.swift index 35f09755..33df4017 100644 --- a/Sources/ContainerCommands/Container/ContainerLogs.swift +++ b/Sources/ContainerCommands/Container/ContainerLogs.swift @@ -47,7 +47,8 @@ extension Application { public func run() async throws { do { - let container = try await ClientContainer.get(id: containerId) + let snapshot = try await ClientContainer.get(id: containerId) + let container = ClientContainer(snapshot: snapshot) let fhs = try await container.logs() let fileHandle = boot ? fhs[1] : fhs[0] diff --git a/Sources/ContainerCommands/Container/ContainerRun.swift b/Sources/ContainerCommands/Container/ContainerRun.swift index ec65329c..d572bddc 100644 --- a/Sources/ContainerCommands/Container/ContainerRun.swift +++ b/Sources/ContainerCommands/Container/ContainerRun.swift @@ -103,11 +103,12 @@ extension Application { progress.set(description: "Starting container") let options = ContainerCreateOptions(autoRemove: managementFlags.remove) - let container = try await ClientContainer.create( + let snapshot = try await ClientContainer.create( configuration: ck.0, options: options, kernel: ck.1 ) + let container = ClientContainer(snapshot: snapshot) let detach = self.managementFlags.detach do { diff --git a/Sources/ContainerCommands/Container/ContainerStart.swift b/Sources/ContainerCommands/Container/ContainerStart.swift index cdef25a9..17aae71d 100644 --- a/Sources/ContainerCommands/Container/ContainerStart.swift +++ b/Sources/ContainerCommands/Container/ContainerStart.swift @@ -53,12 +53,12 @@ extension Application { progress.start() let detach = !self.attach && !self.interactive - let container = try await ClientContainer.get(id: containerId) + let snapshot = try await ClientContainer.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 // support attach currently, so we can't do `start -a` a second time and have it succeed. - if container.status == .running { + if snapshot.status == .running { if !detach { throw ContainerizationError( .invalidArgument, @@ -69,9 +69,11 @@ extension Application { return } + let container = ClientContainer(snapshot: snapshot) + do { let io = try ProcessIO.create( - tty: container.configuration.initProcess.terminal, + tty: snapshot.configuration.initProcess.terminal, interactive: self.interactive, detach: detach ) diff --git a/Sources/ContainerCommands/Container/ContainerStop.swift b/Sources/ContainerCommands/Container/ContainerStop.swift index c10b426b..1fe08ce3 100644 --- a/Sources/ContainerCommands/Container/ContainerStop.swift +++ b/Sources/ContainerCommands/Container/ContainerStop.swift @@ -16,6 +16,7 @@ import ArgumentParser import ContainerClient +import ContainerXPC import ContainerizationError import ContainerizationOS import Foundation @@ -55,12 +56,12 @@ extension Application { public mutating func run() async throws { let set = Set(containerIds) - var containers = [ClientContainer]() + var snapshots = [ContainerSnapshot]() if self.all { - containers = try await ClientContainer.list() + snapshots = try await ClientContainer.list() } else { - containers = try await ClientContainer.list().filter { c in - set.contains(c.id) + snapshots = try await ClientContainer.list().filter { c in + set.contains(c.configuration.id) } } @@ -68,7 +69,7 @@ extension Application { timeoutInSeconds: self.time, signal: try Signals.parseSignal(self.signal) ) - let failed = try await Self.stopContainers(containers: containers, stopOptions: opts) + let failed = try await Self.stopContainers(snapshots: snapshots, stopOptions: opts) if failed.count > 0 { throw ContainerizationError( .internalError, @@ -77,27 +78,30 @@ extension Application { } } - static func stopContainers(containers: [ClientContainer], stopOptions: ContainerStopOptions) async throws -> [String] { + static func stopContainers(snapshots: [ContainerSnapshot], stopOptions: ContainerStopOptions) async throws -> [String] { var failed: [String] = [] - try await withThrowingTaskGroup(of: ClientContainer?.self) { group in - for container in containers { + let sharedClient = XPCClient(service: ClientContainer.serviceIdentifier) + + try await withThrowingTaskGroup(of: String?.self) { group in + for snapshot in snapshots { group.addTask { do { + let container = ClientContainer(snapshot: snapshot, xpcClient: sharedClient) try await container.stop(opts: stopOptions) - print(container.id) + print(snapshot.configuration.id) return nil } catch { - log.error("failed to stop container \(container.id): \(error)") - return container + log.error("failed to stop container \(snapshot.configuration.id): \(error)") + return snapshot.configuration.id } } } - for try await ctr in group { - guard let ctr else { + for try await id in group { + guard let id else { continue } - failed.append(ctr.id) + failed.append(id) } } diff --git a/Sources/ContainerCommands/Container/ProcessUtils.swift b/Sources/ContainerCommands/Container/ProcessUtils.swift index a2cb981d..5aed945b 100644 --- a/Sources/ContainerCommands/Container/ProcessUtils.swift +++ b/Sources/ContainerCommands/Container/ProcessUtils.swift @@ -21,9 +21,9 @@ import ContainerizationOS import Foundation extension Application { - static func ensureRunning(container: ClientContainer) throws { - if container.status != .running { - throw ContainerizationError(.invalidState, message: "container \(container.id) is not running") + static func ensureRunning(snapshot: ContainerSnapshot) throws { + if snapshot.status != .running { + throw ContainerizationError(.invalidState, message: "container \(snapshot.configuration.id) is not running") } } } diff --git a/Sources/ContainerCommands/System/SystemStop.swift b/Sources/ContainerCommands/System/SystemStop.swift index a6617c72..45ab7f8d 100644 --- a/Sources/ContainerCommands/System/SystemStop.swift +++ b/Sources/ContainerCommands/System/SystemStop.swift @@ -62,10 +62,10 @@ extension Application { if running { log.info("stopping containers", metadata: ["stopTimeoutSeconds": "\(Self.stopTimeoutSeconds)"]) do { - let containers = try await ClientContainer.list() + let snapshots = try await ClientContainer.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) + let failed = try await ContainerStop.stopContainers(snapshots: snapshots, stopOptions: opts) if !failed.isEmpty { log.warning("some containers could not be stopped gracefully", metadata: ["ids": "\(failed)"]) }