APIServer: Add support for filtering to list rpc (#1175)

This is not intended to be used to support `--filter` or similar on the
CLIs list yet, it's solely to clean up our rather awkward use of
`ContainerClient.list()` today in the CLI. The list RPC simply returns
all of the containers we have created. Because of this, for a LOT of our
commands we filter to what we need client side, which feels like a
waste.. This change introduces a filter struct that we can provide an
array of container IDs, labels, and the status of the containers to
filter the `list()` output from.

This additionally, because it was killing (pun not intended) me and I
was already having to change this area for the `list()` additions,
changes container kill slightly to return an error if you try and kill a
container that doesn't exist.
This commit is contained in:
Danny Canter
2026-02-11 15:09:21 -08:00
committed by GitHub
parent c9f81ca332
commit f7d00aad48
9 changed files with 101 additions and 47 deletions
@@ -75,10 +75,12 @@ public struct ContainerClient: Sendable {
}
}
/// List all containers.
public func list() async throws -> [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"
@@ -124,6 +124,9 @@ public enum XPCKeys: String {
case statistics
case containerSize
/// Container list filters
case listFilters
/// Disk usage
case diskUsageStats
}
@@ -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()
@@ -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