mirror of
https://github.com/apple/container.git
synced 2026-07-12 12:37:02 +00:00
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:
@@ -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))
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Filters for listing containers.
|
||||
public struct ContainerListFilters: Sendable, Codable {
|
||||
/// Filter by container IDs. If non-empty, only containers with matching IDs are returned.
|
||||
public var ids: [String]
|
||||
/// Filter by container status.
|
||||
public var status: RuntimeStatus?
|
||||
/// Filter by labels. All specified labels must match.
|
||||
public var labels: [String: String]
|
||||
|
||||
/// No filters applied. Will return all containers.
|
||||
public static let all = ContainerListFilters()
|
||||
|
||||
public init(
|
||||
ids: [String] = [],
|
||||
status: RuntimeStatus? = nil,
|
||||
labels: [String: String] = [:]
|
||||
) {
|
||||
self.ids = ids
|
||||
self.status = status
|
||||
self.labels = labels
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user