Make inspect error handling for missing resources consistent (#1564)

- Closes #1539.
- All `inspect` commands now consistently return
  exit 1 with a clear error when a requested resource
  is missing.
This commit is contained in:
Raj
2026-05-16 08:28:37 -07:00
committed by GitHub
parent 5b83b4a9ac
commit bd1916f9b3
8 changed files with 77 additions and 45 deletions
@@ -17,8 +17,8 @@
import ArgumentParser
import ContainerAPIClient
import ContainerResource
import ContainerizationError
import Foundation
import SwiftProtobuf
extension Application {
public struct ContainerInspect: AsyncLoggableCommand {
@@ -36,12 +36,21 @@ extension Application {
public func run() async throws {
let client = ContainerClient()
let uniqueIds = Set(containerIds)
let containers = try await client.list().filter {
containerIds.contains($0.id)
}.map {
PrintableContainer($0)
uniqueIds.contains($0.id)
}
try Output.emit(Output.renderJSON(containers))
if containers.count != uniqueIds.count {
let found = Set(containers.map { $0.id })
let missing = uniqueIds.subtracting(found).sorted()
throw ContainerizationError(
.notFound,
message: "container not found: \(missing.joined(separator: ", "))"
)
}
try Output.emit(Output.renderJSON(containers.map { PrintableContainer($0) }))
}
}
}
@@ -16,14 +16,10 @@
import ArgumentParser
import ContainerAPIClient
import ContainerLog
import ContainerPersistence
import ContainerPlugin
import ContainerResource
import ContainerizationError
import Foundation
import Logging
import SwiftProtobuf
extension Application {
public struct ImageInspect: AsyncLoggableCommand {
@@ -39,19 +35,22 @@ extension Application {
public init() {}
struct InspectError: Error {
let succeeded: [String]
let failed: [(String, Error)]
}
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
let uniqueNames = Set(images)
let result = try await ClientImage.get(
names: Array(uniqueNames), containerSystemConfig: containerSystemConfig
)
if !result.error.isEmpty {
let missing = result.error.sorted()
throw ContainerizationError(
.notFound,
message: "image not found: \(missing.joined(separator: ", "))"
)
}
var printable: [ImageDetail] = []
var succeededImages: [String] = []
var allErrors: [(String, Error)] = []
let result = try await ClientImage.get(names: images, containerSystemConfig: containerSystemConfig)
for image in result.images {
guard
!Utility.isInfraImage(
@@ -61,29 +60,9 @@ extension Application {
)
else { continue }
printable.append(try await image.details())
succeededImages.append(image.reference)
}
for missing in result.error {
allErrors.append((missing, ContainerizationError(.notFound, message: "Image not found")))
}
if !printable.isEmpty {
try Output.emit(Output.renderJSON(printable))
}
if !allErrors.isEmpty {
for (name, error) in allErrors {
log.error(
"image inspect failed",
metadata: [
"name": "\(name)",
"error": "\(error.localizedDescription)",
])
}
throw InspectError(succeeded: succeededImages, failed: allErrors)
}
try Output.emit(Output.renderJSON(printable))
}
}
}
@@ -16,6 +16,7 @@
import ArgumentParser
import ContainerAPIClient
import ContainerizationError
import Foundation
extension Application {
@@ -34,7 +35,18 @@ extension Application {
public func run() async throws {
let networkClient = NetworkClient()
let items = try await networkClient.list().filter { networks.contains($0.id) }
let uniqueNames = Set(networks)
let items = try await networkClient.list().filter { uniqueNames.contains($0.id) }
if items.count != uniqueNames.count {
let found = Set(items.map { $0.id })
let missing = uniqueNames.subtracting(found).sorted()
throw ContainerizationError(
.notFound,
message: "network not found: \(missing.joined(separator: ", "))"
)
}
try Output.emit(Output.renderJSON(items))
}
}
@@ -17,6 +17,7 @@
import ArgumentParser
import ContainerAPIClient
import ContainerResource
import ContainerizationError
import Foundation
extension Application.VolumeCommand {
@@ -35,11 +36,16 @@ extension Application.VolumeCommand {
public init() {}
public func run() async throws {
var volumes: [Volume] = []
let uniqueNames = Set(names)
let volumes = try await ClientVolume.list().filter { uniqueNames.contains($0.id) }
for name in names {
let volume = try await ClientVolume.inspect(name)
volumes.append(volume)
if volumes.count != uniqueNames.count {
let found = Set(volumes.map { $0.id })
let missing = uniqueNames.subtracting(found).sorted()
throw ContainerizationError(
.notFound,
message: "volume not found: \(missing.joined(separator: ", "))"
)
}
let options = JSONOptions(
@@ -132,4 +132,10 @@ class TestCLIRemove: CLITest {
let lines = output.split(separator: "\n").filter { $0.contains(name) }
#expect(lines.count == 1, "Expected container to be deleted exactly once, got \(lines.count) lines")
}
@Test func testInspectMissingContainerFails() throws {
let (_, _, error, status) = try run(arguments: ["inspect", "definitely-missing-container"])
#expect(status != 0, "Expected non-zero exit for missing container")
#expect(error.contains("container not found"))
}
}
@@ -593,4 +593,10 @@ class TestCLIImagesCommand: CLITest {
try FileManager.default.removeItem(atPath: tarPath)
try FileManager.default.moveItem(at: tempModifiedTar, to: URL(fileURLWithPath: tarPath))
}
@Test func testInspectMissingImageFails() throws {
let (_, _, error, status) = try run(arguments: ["image", "inspect", "definitely-missing-image:latest"])
#expect(status != 0, "Expected non-zero exit for missing image")
#expect(error.contains("image not found"))
}
}
@@ -313,4 +313,10 @@ class TestCLINetwork: CLITest {
}
#expect(json.contains { ($0["id"] as? String) == name }, "JSON should contain the created network")
}
@Test func testInspectMissingNetworkFails() throws {
let (_, _, error, status) = try run(arguments: ["network", "inspect", "definitely-missing-network"])
#expect(status != 0, "Expected non-zero exit for missing network")
#expect(error.contains("network not found"))
}
}
@@ -466,6 +466,14 @@ class TestCLIVolumes: CLITest {
#expect(error.contains("conflict"))
}
// MARK: - Inspect validation tests
@Test func testVolumeInspectMissingFails() throws {
let (_, _, error, status) = try run(arguments: ["volume", "inspect", "definitely-missing-volume"])
#expect(status != 0, "Expected non-zero exit for missing volume")
#expect(error.contains("volume not found"))
}
// MARK: - Journal option tests
@Test func testVolumeCreateWithJournalOrdered() throws {