diff --git a/Sources/ContainerCommands/Container/ContainerKill.swift b/Sources/ContainerCommands/Container/ContainerKill.swift index bba73213..2122c115 100644 --- a/Sources/ContainerCommands/Container/ContainerKill.swift +++ b/Sources/ContainerCommands/Container/ContainerKill.swift @@ -16,6 +16,7 @@ import ArgumentParser import ContainerAPIClient +import ContainerResource import ContainerizationError import ContainerizationOS import Darwin @@ -50,16 +51,13 @@ extension Application { } public mutating func run() async throws { - let set = Set(containerIds) let client = ContainerClient() - var containers = try await client.list().filter { c in - c.status == .running - } - if !self.all { - containers = containers.filter { c in - set.contains(c.id) - } + let containers: [String] + if self.all { + containers = try await client.list(filters: ContainerListFilters(status: .running)).map { $0.id } + } else { + containers = containerIds } let signalNumber = try Signals.parseSignal(signal) @@ -67,8 +65,8 @@ extension Application { var errors: [any Error] = [] for container in containers { do { - try await client.kill(id: container.id, signal: signalNumber) - print(container.id) + try await client.kill(id: container, signal: signalNumber) + print(container) } catch { errors.append(error) } diff --git a/Sources/ContainerCommands/Container/ContainerList.swift b/Sources/ContainerCommands/Container/ContainerList.swift index e680d4c7..53b110bf 100644 --- a/Sources/ContainerCommands/Container/ContainerList.swift +++ b/Sources/ContainerCommands/Container/ContainerList.swift @@ -44,7 +44,8 @@ extension Application { public func run() async throws { let client = ContainerClient() - let containers = try await client.list() + let filters = self.all ? ContainerListFilters.all : ContainerListFilters(status: .running) + let containers = try await client.list(filters: filters) try printContainers(containers: containers, format: format) } @@ -65,9 +66,6 @@ extension Application { if self.quiet { containers.forEach { - if !self.all && $0.status != .running { - return - } print($0.id) } return @@ -75,9 +73,6 @@ extension Application { var rows = createHeader() for container in containers { - if !self.all && container.status != .running { - continue - } rows.append(container.asRow) } diff --git a/Sources/ContainerCommands/Container/ContainerStats.swift b/Sources/ContainerCommands/Container/ContainerStats.swift index a7ab77f7..f424689c 100644 --- a/Sources/ContainerCommands/Container/ContainerStats.swift +++ b/Sources/ContainerCommands/Container/ContainerStats.swift @@ -63,25 +63,23 @@ extension Application { private func runStatic() async throws { let client = ContainerClient() - let allContainers = try await client.list() let containersToShow: [ContainerSnapshot] if containers.isEmpty { // No containers specified - show all running containers - containersToShow = allContainers.filter { $0.status == .running } + containersToShow = try await client.list(filters: ContainerListFilters(status: .running)) } else { - // Validate all specified containers exist before proceeding - var found: [ContainerSnapshot] = [] + // Fetch specified containers by ID + containersToShow = try await client.list(filters: ContainerListFilters(ids: containers)) + // Validate all specified containers were found for containerId in containers { - guard let container = allContainers.first(where: { $0.id == containerId || $0.id.starts(with: containerId) }) else { + guard containersToShow.contains(where: { $0.id == containerId }) else { throw ContainerizationError( .notFound, message: "no such container: \(containerId)" ) } - found.append(container) } - containersToShow = found } let statsData = try await collectStats(client: client, for: containersToShow) @@ -101,9 +99,9 @@ extension Application { // If containers were specified, validate they all exist upfront if !containers.isEmpty { - let allContainers = try await client.list() + let specifiedContainers = try await client.list(filters: ContainerListFilters(ids: containers)) for containerId in containers { - guard allContainers.first(where: { $0.id == containerId || $0.id.starts(with: containerId) }) != nil else { + guard specifiedContainers.contains(where: { $0.id == containerId }) else { throw ContainerizationError( .notFound, message: "no such container: \(containerId)" @@ -118,19 +116,11 @@ extension Application { while true { do { - let allContainers = try await client.list() - let containersToShow: [ContainerSnapshot] if containers.isEmpty { - containersToShow = allContainers.filter { $0.status == .running } + containersToShow = try await client.list(filters: ContainerListFilters(status: .running)) } else { - var found: [ContainerSnapshot] = [] - for containerId in containers { - if let container = allContainers.first(where: { $0.id == containerId || $0.id.starts(with: containerId) }) { - found.append(container) - } - } - containersToShow = found + containersToShow = try await client.list(filters: ContainerListFilters(ids: containers)) } let statsData = try await collectStats(client: client, for: containersToShow) diff --git a/Sources/ContainerCommands/System/SystemStop.swift b/Sources/ContainerCommands/System/SystemStop.swift index ec8e140b..73cdc28a 100644 --- a/Sources/ContainerCommands/System/SystemStop.swift +++ b/Sources/ContainerCommands/System/SystemStop.swift @@ -79,9 +79,8 @@ extension Application { log.info("waiting for containers to exit") do { for _ in 0.. [ContainerSnapshot] { + /// List containers matching the given filters. + public func list(filters: ContainerListFilters = .all) async throws -> [ContainerSnapshot] { do { let request = XPCMessage(route: .containerList) + let filterData = try JSONEncoder().encode(filters) + request.set(key: .listFilters, value: filterData) let response = try await xpcSend( message: request, @@ -100,8 +102,8 @@ public struct ContainerClient: Sendable { /// Get the container for the provided id. public func get(id: String) async throws -> ContainerSnapshot { - let containers = try await list() - guard let container = containers.first(where: { $0.configuration.id == id }) else { + let containers = try await list(filters: ContainerListFilters(ids: [id])) + guard let container = containers.first else { throw ContainerizationError( .notFound, message: "get failed: container \(id) not found" diff --git a/Sources/Services/ContainerAPIService/Client/XPC+.swift b/Sources/Services/ContainerAPIService/Client/XPC+.swift index 49edbadf..d20a0c95 100644 --- a/Sources/Services/ContainerAPIService/Client/XPC+.swift +++ b/Sources/Services/ContainerAPIService/Client/XPC+.swift @@ -124,6 +124,9 @@ public enum XPCKeys: String { case statistics case containerSize + /// Container list filters + case listFilters + /// Disk usage case diskUsageStats } diff --git a/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift b/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift index e5483872..6fffedbe 100644 --- a/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift +++ b/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift @@ -33,7 +33,11 @@ public struct ContainersHarness: Sendable { @Sendable public func list(_ message: XPCMessage) async throws -> XPCMessage { - let containers = try await service.list() + var filters = ContainerListFilters.all + if let filterData = message.dataNoCopy(key: .listFilters) { + filters = try JSONDecoder().decode(ContainerListFilters.self, from: filterData) + } + let containers = try await service.list(filters: filters) let data = try JSONEncoder().encode(containers) let reply = message.reply() diff --git a/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift b/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift index 17eb2ba8..8a2009f3 100644 --- a/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift +++ b/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift @@ -106,10 +106,33 @@ public actor ContainersService { return results } - /// List all containers registered with the service. - public func list() async throws -> [ContainerSnapshot] { + /// List containers matching the given filters. + public func list(filters: ContainerListFilters = .all) async throws -> [ContainerSnapshot] { self.log.debug("\(#function)") - return self.containers.values.map { $0.snapshot } + + return self.containers.values.compactMap { state -> ContainerSnapshot? in + let snapshot = state.snapshot + + if !filters.ids.isEmpty { + guard filters.ids.contains(snapshot.id) else { + return nil + } + } + + if let status = filters.status { + guard snapshot.status == status else { + return nil + } + } + + for (key, value) in filters.labels { + guard snapshot.configuration.labels[key] == value else { + return nil + } + } + + return snapshot + } } /// Execute an operation with the current container list while maintaining atomicity