fix: handle container clean failing on read-only named volume mounts (#2228)

- Skips the rootfs when the container was created with
  `--read-only`, and skips block mounts whose options
  include `ro`.
- Keeps trimming the remaining targets after a failure,
  then throws one error listing each failed path with its
  underlying error.
This commit is contained in:
Di Warachet S.
2026-09-02 15:46:16 -07:00
committed by GitHub
parent b8ffd38c73
commit c82fe3c4be
2 changed files with 77 additions and 9 deletions
@@ -860,7 +860,7 @@ public actor RuntimeService {
self.log.info("`clean` xpc handler")
switch self.state {
case .running:
guard message.string(key: RuntimeKeys.id.rawValue) != nil else {
guard let id = message.string(key: RuntimeKeys.id.rawValue) else {
throw ContainerizationError(
.invalidArgument,
message: "no id supplied for clean"
@@ -869,18 +869,31 @@ public actor RuntimeService {
let ctr = try getContainer()
// Perform filesystem trim on the root filesystem
try await ctr.container.filesystemOperation(operation: .trim, path: "/")
var targets: [String] = []
if !ctr.config.readOnly {
targets.append("/")
}
for mount in ctr.config.mounts where mount.isBlock && !mount.options.readonly {
targets.append(mount.destination)
}
// Trim all block-backed mounts. Named volumes are expected to be
// block-backed, and may be represented as either `.volume` or
// `.block` depending on how configuration was created.
for mount in ctr.config.mounts {
if mount.isBlock {
try await ctr.container.filesystemOperation(operation: .trim, path: mount.destination)
var failed: [String] = []
for path in targets {
do {
try await ctr.container.filesystemOperation(operation: .trim, path: path)
} catch {
self.log.error("failed to clean mount", metadata: ["path": "\(path)", "error": "\(error)"])
failed.append("\(path) (\(error))")
}
}
guard failed.isEmpty else {
throw ContainerizationError(
.internalError,
message: "failed to clean mounts in \(id): \(failed.joined(separator: ", "))"
)
}
return message.reply()
default:
throw ContainerizationError(
@@ -111,6 +111,61 @@ struct TestCLIClean {
}
}
@Test func testCleanWithReadOnlyRootfs() async throws {
try await ContainerFixture.with { f in
try await f.withContainer(image: WarmupImage.alpine320.rawValue, runArgs: ["--read-only"]) { name in
try f.doClean(name)
#expect(try f.getContainerStatus(name) == "running")
}
}
}
@Test func testCleanWithMixedVolumes() async throws {
try await ContainerFixture.with { f in
let rwVolume = "\(f.testID)-rw-vol"
let roVolume = "\(f.testID)-ro-vol"
try f.doVolumeCreate(rwVolume)
try f.doVolumeCreate(roVolume)
f.addCleanup { f.doVolumeDeleteIfExists(rwVolume) }
f.addCleanup { f.doVolumeDeleteIfExists(roVolume) }
try await f.withContainer(
image: WarmupImage.alpine320.rawValue,
runArgs: ["-v", "\(rwVolume):/rw", "-v", "\(roVolume):/ro:ro"]
) { name in
let rwBlockURL = try volumeBlockURL(f, name: rwVolume)
let beforeWrite = try allocatedBytes(at: rwBlockURL)
try f.doExec(name, cmd: ["sh", "-c", "dd if=/dev/urandom of=/rw/test bs=1M count=5"])
try f.doExec(name, cmd: ["sync"])
let afterWrite = try allocatedBytes(at: rwBlockURL)
try f.doExec(name, cmd: ["rm", "/rw/test"])
try f.doClean(name)
try f.doExec(name, cmd: ["sync"])
let afterClean = try allocatedBytes(at: rwBlockURL)
assertCleanReclaimedSpace(beforeWrite: beforeWrite, afterWrite: afterWrite, afterClean: afterClean)
#expect(try f.getContainerStatus(name) == "running")
}
}
}
@Test func testCleanWithReadOnlyVolume() async throws {
try await ContainerFixture.with { f in
let volumeName = "\(f.testID)-ro-vol"
try f.doVolumeCreate(volumeName)
f.addCleanup { f.doVolumeDeleteIfExists(volumeName) }
try await f.withContainer(
image: WarmupImage.alpine320.rawValue,
runArgs: ["-v", "\(volumeName):/mnt/vol:ro"]
) { name in
try f.doClean(name)
#expect(try f.getContainerStatus(name) == "running")
}
}
}
@Test func testCleanWithVolume() async throws {
try await ContainerFixture.with { f in
let volumeName = "\(f.testID)-vol"