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
@@ -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<String>(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)
}
@@ -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)
}
@@ -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)
@@ -79,9 +79,8 @@ extension Application {
log.info("waiting for containers to exit")
do {
for _ in 0..<Self.shutdownTimeoutSeconds {
let anyRunning = try await client.list()
.contains { $0.status == .running }
guard anyRunning else {
let runningContainers = try await client.list(filters: ContainerListFilters(status: .running))
guard !runningContainers.isEmpty else {
break
}
try await Task.sleep(for: .seconds(1))