Skip to content

Commit 82d2df0

Browse files
committed
Fix container clean runtime identity and integration coverage
1 parent 053190c commit 82d2df0

4 files changed

Lines changed: 93 additions & 17 deletions

File tree

Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -928,7 +928,7 @@ public actor ContainersService {
928928
}
929929

930930
let client = try state.getClient()
931-
try await client.clean(id: id)
931+
try await client.clean()
932932
}
933933

934934
private func handleContainerExit(id: String, code: ExitStatus? = nil) async throws {

Sources/Services/Runtime/RuntimeClient/RuntimeClient.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,9 +359,9 @@ extension RuntimeClient {
359359
return try JSONDecoder().decode(ContainerStats.self, from: data)
360360
}
361361

362-
public func clean(id: String) async throws {
362+
public func clean() async throws {
363363
let request = XPCMessage(route: RuntimeRoutes.clean.rawValue)
364-
request.set(key: RuntimeKeys.id.rawValue, value: id)
364+
request.set(key: RuntimeKeys.id.rawValue, value: self.id)
365365

366366
do {
367367
try await self.client.send(request)

Sources/Services/RuntimeLinux/Server/RuntimeService.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,14 +794,20 @@ public actor RuntimeService {
794794
self.log.info("`clean` xpc handler")
795795
switch self.state {
796796
case .running:
797-
guard message.string(key: RuntimeKeys.id.rawValue) != nil else {
797+
guard let id = message.string(key: RuntimeKeys.id.rawValue) else {
798798
throw ContainerizationError(
799799
.invalidArgument,
800800
message: "no id supplied for clean"
801801
)
802802
}
803803

804804
let ctr = try getContainer()
805+
guard id == ctr.config.id else {
806+
throw ContainerizationError(
807+
.invalidArgument,
808+
message: "clean id does not match runtime container"
809+
)
810+
}
805811

806812
// Perform filesystem trim on the root filesystem
807813
try await ctr.container.filesystemOperation(operation: .trim, path: "/")

Tests/IntegrationTests/Containers/TestCLIClean.swift

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
//===----------------------------------------------------------------------===//
1616

1717
import ContainerTestSupport
18+
import Darwin
1819
import Foundation
1920
import Testing
2021

@@ -31,12 +32,11 @@ struct TestCLIClean {
3132
}
3233

3334
private func allocatedBytes(at url: URL) throws -> Int64 {
34-
let values = try url.resourceValues(forKeys: [.fileAllocatedSizeKey, .totalFileAllocatedSizeKey])
35-
let allocated = values.totalFileAllocatedSize ?? values.fileAllocatedSize
36-
guard let allocated else {
37-
throw CommandError.executionFailed("failed to read allocated size for \(url.path)")
35+
var fileStatus = stat()
36+
guard lstat(url.path, &fileStatus) == 0 else {
37+
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
3838
}
39-
return Int64(allocated)
39+
return Int64(fileStatus.st_blocks) * 512
4040
}
4141

4242
private func containerRootfsBlockURL(_ fixture: ContainerFixture, name: String) throws -> URL {
@@ -67,6 +67,63 @@ struct TestCLIClean {
6767
"clean should reclaim at least 80% of storage allocated by the test write")
6868
}
6969

70+
private func waitForStableAllocatedSpace(
71+
at url: URL,
72+
timeout: TimeInterval = 10
73+
) async throws -> Int64 {
74+
let deadline = Date.now.addingTimeInterval(timeout)
75+
var previous = try allocatedBytes(at: url)
76+
var unchangedSamples = 0
77+
78+
while Date.now < deadline {
79+
try await Task.sleep(for: .milliseconds(250))
80+
let current = try allocatedBytes(at: url)
81+
if current == previous {
82+
unchangedSamples += 1
83+
if unchangedSamples == 4 {
84+
return current
85+
}
86+
} else {
87+
previous = current
88+
unchangedSamples = 0
89+
}
90+
}
91+
return previous
92+
}
93+
94+
private func waitForAllocatedSpace(
95+
after baseline: Int64,
96+
at url: URL,
97+
timeout: TimeInterval = 10
98+
) async throws -> Int64 {
99+
let deadline = Date.now.addingTimeInterval(timeout)
100+
var allocated = try allocatedBytes(at: url)
101+
102+
while allocated <= baseline, Date.now < deadline {
103+
try await Task.sleep(for: .milliseconds(250))
104+
allocated = try allocatedBytes(at: url)
105+
}
106+
return allocated
107+
}
108+
109+
private func waitForReclaimedSpace(
110+
beforeWrite: Int64,
111+
afterWrite: Int64,
112+
at url: URL,
113+
timeout: TimeInterval = 10
114+
) async throws -> Int64 {
115+
let allocatedByWrite = afterWrite - beforeWrite
116+
let minimumExpectedReclaimed = Int64(Double(allocatedByWrite) * 0.8)
117+
let deadline = Date.now.addingTimeInterval(timeout)
118+
var afterClean = try allocatedBytes(at: url)
119+
120+
while afterWrite - afterClean < minimumExpectedReclaimed, Date.now < deadline {
121+
try await Task.sleep(for: .milliseconds(250))
122+
afterClean = try allocatedBytes(at: url)
123+
}
124+
return afterClean
125+
}
126+
70127
@Test func cleanRejectsStoppedContainer() async throws {
71128
try await ContainerFixture.with { fixture in
72129
let name = "\(fixture.testID)-clean-stopped"
@@ -81,6 +138,9 @@ struct TestCLIClean {
81138

82139
let result = try fixture.run(["clean", name])
83140
#expect(result.status != 0, "clean should reject a stopped container")
141+
#expect(
142+
result.error.contains("not running"),
143+
"clean should report that the stopped container is not running; stderr: \(result.error)")
84144
}
85145
}
86146

@@ -117,18 +177,23 @@ struct TestCLIClean {
117177
fixture.addCleanup { try? fixture.doRemove(name, force: true) }
118178

119179
let rootfsBlockURL = try containerRootfsBlockURL(fixture, name: name)
120-
let beforeWrite = try allocatedBytes(at: rootfsBlockURL)
180+
try fixture.doClean(name)
181+
let beforeWrite = try await waitForStableAllocatedSpace(at: rootfsBlockURL)
121182

122183
try fixture.doExec(
123184
name,
124-
cmd: ["sh", "-c", "dd if=/dev/urandom of=/rootfs-reclaim.dat bs=1M count=64"])
185+
cmd: ["sh", "-c", "dd if=/dev/urandom of=/rootfs-reclaim.dat bs=1M count=256"])
125186
try fixture.doExec(name, cmd: ["sync"])
126-
let afterWrite = try allocatedBytes(at: rootfsBlockURL)
187+
let afterWrite = try await waitForAllocatedSpace(after: beforeWrite, at: rootfsBlockURL)
127188

128189
try fixture.doExec(name, cmd: ["rm", "/rootfs-reclaim.dat"])
129190
try fixture.doExec(name, cmd: ["sync"])
130191
try fixture.doClean(name)
131-
let afterClean = try allocatedBytes(at: rootfsBlockURL)
192+
let afterClean = try await waitForReclaimedSpace(
193+
beforeWrite: beforeWrite,
194+
afterWrite: afterWrite,
195+
at: rootfsBlockURL)
196+
print("rootfs allocated bytes before=\(beforeWrite) afterWrite=\(afterWrite) afterClean=\(afterClean)")
132197

133198
expectReclaimedSpace(
134199
beforeWrite: beforeWrite,
@@ -154,18 +219,23 @@ struct TestCLIClean {
154219
try await fixture.waitForContainerRunning(name)
155220

156221
let volumeBlockURL = try volumeBlockURL(fixture, name: volumeName)
157-
let beforeWrite = try allocatedBytes(at: volumeBlockURL)
222+
try fixture.doClean(name)
223+
let beforeWrite = try await waitForStableAllocatedSpace(at: volumeBlockURL)
158224

159225
try fixture.doExec(
160226
name,
161-
cmd: ["sh", "-c", "dd if=/dev/urandom of=/mnt/reclaim-data/volume-reclaim.dat bs=1M count=64"])
227+
cmd: ["sh", "-c", "dd if=/dev/urandom of=/mnt/reclaim-data/volume-reclaim.dat bs=1M count=256"])
162228
try fixture.doExec(name, cmd: ["sync"])
163-
let afterWrite = try allocatedBytes(at: volumeBlockURL)
229+
let afterWrite = try await waitForAllocatedSpace(after: beforeWrite, at: volumeBlockURL)
164230

165231
try fixture.doExec(name, cmd: ["rm", "/mnt/reclaim-data/volume-reclaim.dat"])
166232
try fixture.doExec(name, cmd: ["sync"])
167233
try fixture.doClean(name)
168-
let afterClean = try allocatedBytes(at: volumeBlockURL)
234+
let afterClean = try await waitForReclaimedSpace(
235+
beforeWrite: beforeWrite,
236+
afterWrite: afterWrite,
237+
at: volumeBlockURL)
238+
print("volume allocated bytes before=\(beforeWrite) afterWrite=\(afterWrite) afterClean=\(afterClean)")
169239

170240
expectReclaimedSpace(
171241
beforeWrite: beforeWrite,

0 commit comments

Comments
 (0)