diff --git a/Sources/Services/RuntimeLinux/Server/RuntimeService.swift b/Sources/Services/RuntimeLinux/Server/RuntimeService.swift index 79857712..951404a3 100644 --- a/Sources/Services/RuntimeLinux/Server/RuntimeService.swift +++ b/Sources/Services/RuntimeLinux/Server/RuntimeService.swift @@ -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( diff --git a/Tests/IntegrationTests/Containers/TestCLIClean.swift b/Tests/IntegrationTests/Containers/TestCLIClean.swift index f1ada3e6..34ef0c8c 100644 --- a/Tests/IntegrationTests/Containers/TestCLIClean.swift +++ b/Tests/IntegrationTests/Containers/TestCLIClean.swift @@ -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"