mirror of
https://github.com/apple/container.git
synced 2026-09-12 18:55:43 +00:00
Taking in a filehandle gives the user quite a bit more freedom on how to handle boot log output. They can set up a kqueue watch on it and redirect output somewhere else etc etc. The implementation for this has us take in a new BootLog type that has two options: 1. .file, which is analogous to what we had prior. Just provide a URL and a true by default append field. 2. .fileHandle which is the new addition. Can pass any fd that is writable, and the VMM should write serial console output to it.
706 lines
27 KiB
Swift
706 lines
27 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025 Apple Inc. and the Containerization 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 ArgumentParser
|
|
import Containerization
|
|
import ContainerizationError
|
|
import ContainerizationOCI
|
|
import ContainerizationOS
|
|
import Foundation
|
|
import Logging
|
|
|
|
extension IntegrationSuite {
|
|
/// Clone a rootfs mount to a new location for use by a container in a pod
|
|
private func cloneRootfs(_ rootfs: Containerization.Mount, testID: String, containerID: String) throws -> Containerization.Mount {
|
|
let clonePath = Self.testDir.appending(component: "\(testID)-\(containerID).ext4").absolutePath()
|
|
try? FileManager.default.removeItem(atPath: clonePath)
|
|
return try rootfs.clone(to: clonePath)
|
|
}
|
|
|
|
func testPodSingleContainer() async throws {
|
|
let id = "test-pod-single-container"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
|
|
config.process.arguments = ["/bin/true"]
|
|
}
|
|
|
|
try await pod.create()
|
|
try await pod.startContainer("container1")
|
|
|
|
let status = try await pod.waitContainer("container1")
|
|
try await pod.stop()
|
|
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "process status \(status) != 0")
|
|
}
|
|
}
|
|
|
|
func testPodMultipleContainers() async throws {
|
|
let id = "test-pod-multiple-containers"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
|
|
config.process.arguments = ["/bin/true"]
|
|
}
|
|
|
|
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
|
|
config.process.arguments = ["/bin/echo", "hello"]
|
|
}
|
|
|
|
try await pod.create()
|
|
|
|
try await pod.startContainer("container1")
|
|
let status1 = try await pod.waitContainer("container1")
|
|
|
|
try await pod.startContainer("container2")
|
|
let status2 = try await pod.waitContainer("container2")
|
|
|
|
try await pod.stop()
|
|
|
|
guard status1.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "container1 status \(status1) != 0")
|
|
}
|
|
|
|
guard status2.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "container2 status \(status2) != 0")
|
|
}
|
|
}
|
|
|
|
func testPodContainerOutput() async throws {
|
|
let id = "test-pod-container-output"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
let buffer = BufferWriter()
|
|
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
|
|
config.process.arguments = ["/bin/echo", "hello from pod"]
|
|
config.process.stdout = buffer
|
|
}
|
|
|
|
try await pod.create()
|
|
try await pod.startContainer("container1")
|
|
|
|
let status = try await pod.waitContainer("container1")
|
|
try await pod.stop()
|
|
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "process status \(status) != 0")
|
|
}
|
|
|
|
guard String(data: buffer.data, encoding: .utf8) == "hello from pod\n" else {
|
|
throw IntegrationError.assert(
|
|
msg: "process should have returned on stdout 'hello from pod' != '\(String(data: buffer.data, encoding: .utf8)!)'")
|
|
}
|
|
}
|
|
|
|
func testPodConcurrentContainers() async throws {
|
|
let id = "test-pod-concurrent-containers"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
// Add 5 containers
|
|
for i in 0..<5 {
|
|
try await pod.addContainer("container\(i)", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container\(i)")) { config in
|
|
config.process.arguments = ["/bin/sleep", "1"]
|
|
}
|
|
}
|
|
|
|
try await pod.create()
|
|
|
|
// Start all containers concurrently
|
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
for i in 0..<5 {
|
|
group.addTask {
|
|
try await pod.startContainer("container\(i)")
|
|
}
|
|
}
|
|
try await group.waitForAll()
|
|
}
|
|
|
|
// Wait for all containers concurrently
|
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
for i in 0..<5 {
|
|
group.addTask {
|
|
let status = try await pod.waitContainer("container\(i)")
|
|
if status.exitCode != 0 {
|
|
throw IntegrationError.assert(msg: "container\(i) status \(status) != 0")
|
|
}
|
|
}
|
|
}
|
|
try await group.waitForAll()
|
|
}
|
|
|
|
try await pod.stop()
|
|
}
|
|
|
|
func testPodExecInContainer() async throws {
|
|
let id = "test-pod-exec-in-container"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
|
|
config.process.arguments = ["/bin/sleep", "100"]
|
|
}
|
|
|
|
try await pod.create()
|
|
try await pod.startContainer("container1")
|
|
|
|
let buffer = BufferWriter()
|
|
let exec = try await pod.execInContainer("container1", processID: "exec1") { config in
|
|
config.arguments = ["/bin/echo", "exec test"]
|
|
config.stdout = buffer
|
|
}
|
|
|
|
try await exec.start()
|
|
let status = try await exec.wait()
|
|
try await exec.delete()
|
|
|
|
try await pod.killContainer("container1", signal: SIGKILL)
|
|
try await pod.waitContainer("container1")
|
|
try await pod.stop()
|
|
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "exec status \(status) != 0")
|
|
}
|
|
|
|
guard String(data: buffer.data, encoding: .utf8) == "exec test\n" else {
|
|
throw IntegrationError.assert(
|
|
msg: "exec should have returned 'exec test' != '\(String(data: buffer.data, encoding: .utf8)!)'")
|
|
}
|
|
}
|
|
|
|
func testPodContainerHostname() async throws {
|
|
let id = "test-pod-container-hostname"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
let buffer = BufferWriter()
|
|
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
|
|
config.process.arguments = ["/bin/hostname"]
|
|
config.hostname = "my-pod-container"
|
|
config.process.stdout = buffer
|
|
}
|
|
|
|
try await pod.create()
|
|
try await pod.startContainer("container1")
|
|
|
|
let status = try await pod.waitContainer("container1")
|
|
try await pod.stop()
|
|
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "process status \(status) != 0")
|
|
}
|
|
|
|
guard String(data: buffer.data, encoding: .utf8) == "my-pod-container\n" else {
|
|
throw IntegrationError.assert(
|
|
msg: "hostname should be 'my-pod-container' != '\(String(data: buffer.data, encoding: .utf8)!)'")
|
|
}
|
|
}
|
|
|
|
func testPodStopContainerIdempotency() async throws {
|
|
let id = "test-pod-stop-container-idempotency"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
|
|
config.process.arguments = ["/bin/true"]
|
|
}
|
|
|
|
try await pod.create()
|
|
try await pod.startContainer("container1")
|
|
|
|
let status = try await pod.waitContainer("container1")
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "process status \(status) != 0")
|
|
}
|
|
|
|
// Stop container twice - should not fail
|
|
try await pod.stopContainer("container1")
|
|
try await pod.stopContainer("container1")
|
|
|
|
try await pod.stop()
|
|
}
|
|
|
|
func testPodListContainers() async throws {
|
|
let id = "test-pod-list-containers"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
let containerIDs = ["container1", "container2", "container3"]
|
|
for containerID in containerIDs {
|
|
try await pod.addContainer(containerID, rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: containerID)) { config in
|
|
config.process.arguments = ["/bin/true"]
|
|
}
|
|
}
|
|
|
|
let listedContainers = await pod.listContainers()
|
|
|
|
guard Set(listedContainers) == Set(containerIDs) else {
|
|
throw IntegrationError.assert(
|
|
msg: "listed containers \(listedContainers) != expected \(containerIDs)")
|
|
}
|
|
|
|
try await pod.create()
|
|
try await pod.stop()
|
|
}
|
|
|
|
func testPodContainerStatistics() async throws {
|
|
let id = "test-pod-container-statistics"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
|
|
config.process.arguments = ["/bin/sleep", "infinity"]
|
|
}
|
|
|
|
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
|
|
config.process.arguments = ["/bin/sleep", "infinity"]
|
|
}
|
|
|
|
do {
|
|
try await pod.create()
|
|
try await pod.startContainer("container1")
|
|
try await pod.startContainer("container2")
|
|
|
|
let stats = try await pod.statistics()
|
|
|
|
guard stats.count == 2 else {
|
|
throw IntegrationError.assert(msg: "expected 2 container stats, got \(stats.count)")
|
|
}
|
|
|
|
let containerIDs = Set(stats.map { $0.id })
|
|
guard containerIDs == Set(["container1", "container2"]) else {
|
|
throw IntegrationError.assert(msg: "unexpected container IDs in stats: \(containerIDs)")
|
|
}
|
|
|
|
for stat in stats {
|
|
guard stat.process.current > 0 else {
|
|
throw IntegrationError.assert(msg: "container \(stat.id) process count should be > 0")
|
|
}
|
|
|
|
guard stat.memory.usageBytes > 0 else {
|
|
throw IntegrationError.assert(msg: "container \(stat.id) memory usage should be > 0")
|
|
}
|
|
|
|
print("Container \(stat.id) statistics:")
|
|
print(" Processes: \(stat.process.current)")
|
|
print(" Memory: \(stat.memory.usageBytes) bytes")
|
|
print(" CPU: \(stat.cpu.usageUsec) usec")
|
|
}
|
|
|
|
try await pod.stop()
|
|
} catch {
|
|
try? await pod.stop()
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func testPodContainerResourceLimits() async throws {
|
|
let id = "test-pod-container-resource-limits"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
|
|
config.process.arguments = ["/bin/sleep", "infinity"]
|
|
config.cpus = 2
|
|
config.memoryInBytes = 256.mib()
|
|
}
|
|
|
|
do {
|
|
try await pod.create()
|
|
try await pod.startContainer("container1")
|
|
|
|
// Verify memory limit
|
|
let memoryBuffer = BufferWriter()
|
|
let memoryExec = try await pod.execInContainer("container1", processID: "check-memory") { config in
|
|
config.arguments = ["cat", "/sys/fs/cgroup/memory.max"]
|
|
config.stdout = memoryBuffer
|
|
}
|
|
try await memoryExec.start()
|
|
var status = try await memoryExec.wait()
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "check-memory status \(status) != 0")
|
|
}
|
|
try await memoryExec.delete()
|
|
|
|
guard let memoryLimit = String(data: memoryBuffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
|
throw IntegrationError.assert(msg: "failed to parse memory.max")
|
|
}
|
|
let expectedMemory = "\(256.mib())"
|
|
guard memoryLimit == expectedMemory else {
|
|
throw IntegrationError.assert(msg: "memory.max \(memoryLimit) != expected \(expectedMemory)")
|
|
}
|
|
|
|
// Verify CPU limit
|
|
let cpuBuffer = BufferWriter()
|
|
let cpuExec = try await pod.execInContainer("container1", processID: "check-cpu") { config in
|
|
config.arguments = ["cat", "/sys/fs/cgroup/cpu.max"]
|
|
config.stdout = cpuBuffer
|
|
}
|
|
try await cpuExec.start()
|
|
status = try await cpuExec.wait()
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "check-cpu status \(status) != 0")
|
|
}
|
|
try await cpuExec.delete()
|
|
|
|
guard let cpuLimit = String(data: cpuBuffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
|
throw IntegrationError.assert(msg: "failed to parse cpu.max")
|
|
}
|
|
let expectedCpu = "200000 100000" // 2 CPUs: quota=200000, period=100000
|
|
guard cpuLimit == expectedCpu else {
|
|
throw IntegrationError.assert(msg: "cpu.max '\(cpuLimit)' != expected '\(expectedCpu)'")
|
|
}
|
|
|
|
try await pod.killContainer("container1", signal: SIGKILL)
|
|
try await pod.waitContainer("container1")
|
|
try await pod.stop()
|
|
} catch {
|
|
try? await pod.stop()
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func testPodContainerFilesystemIsolation() async throws {
|
|
let id = "test-pod-container-filesystem-isolation"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
|
|
config.process.arguments = ["/bin/sleep", "infinity"]
|
|
}
|
|
|
|
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
|
|
config.process.arguments = ["/bin/sleep", "infinity"]
|
|
}
|
|
|
|
do {
|
|
try await pod.create()
|
|
try await pod.startContainer("container1")
|
|
try await pod.startContainer("container2")
|
|
|
|
// Write a file in container1
|
|
let writeExec = try await pod.execInContainer("container1", processID: "write-file") { config in
|
|
config.arguments = ["sh", "-c", "echo 'secret data' > /tmp/container1-secret.txt"]
|
|
}
|
|
try await writeExec.start()
|
|
var status = try await writeExec.wait()
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "write-file status \(status) != 0")
|
|
}
|
|
try await writeExec.delete()
|
|
|
|
// Verify the file exists in container1
|
|
let readBuffer1 = BufferWriter()
|
|
let readExec1 = try await pod.execInContainer("container1", processID: "read-file-1") { config in
|
|
config.arguments = ["cat", "/tmp/container1-secret.txt"]
|
|
config.stdout = readBuffer1
|
|
}
|
|
try await readExec1.start()
|
|
status = try await readExec1.wait()
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "read-file-1 status \(status) != 0")
|
|
}
|
|
try await readExec1.delete()
|
|
|
|
guard String(data: readBuffer1.data, encoding: .utf8) == "secret data\n" else {
|
|
throw IntegrationError.assert(msg: "file content in container1 should be 'secret data'")
|
|
}
|
|
|
|
// Try to read the file from container2 - should fail
|
|
let readExec2 = try await pod.execInContainer("container2", processID: "read-file-2") { config in
|
|
config.arguments = ["cat", "/tmp/container1-secret.txt"]
|
|
}
|
|
try await readExec2.start()
|
|
status = try await readExec2.wait()
|
|
try await readExec2.delete()
|
|
|
|
// File should NOT exist in container2, so cat should fail
|
|
guard status.exitCode != 0 else {
|
|
throw IntegrationError.assert(msg: "file should NOT be accessible from container2")
|
|
}
|
|
|
|
try await pod.stop()
|
|
} catch {
|
|
try? await pod.stop()
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func testPodContainerPIDNamespaceIsolation() async throws {
|
|
let id = "test-pod-container-pid-isolation"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
|
|
config.process.arguments = ["/bin/sleep", "infinity"]
|
|
}
|
|
|
|
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
|
|
config.process.arguments = ["/bin/sleep", "infinity"]
|
|
}
|
|
|
|
do {
|
|
try await pod.create()
|
|
try await pod.startContainer("container1")
|
|
try await pod.startContainer("container2")
|
|
|
|
// Start a unique process in container1
|
|
let sleepExec1 = try await pod.execInContainer("container1", processID: "unique-sleep-1") { config in
|
|
config.arguments = ["/bin/sleep", "9999"]
|
|
}
|
|
try await sleepExec1.start()
|
|
|
|
// List processes in container1 - should see sleep 9999
|
|
let ps1Buffer = BufferWriter()
|
|
let psExec1 = try await pod.execInContainer("container1", processID: "ps-1") { config in
|
|
config.arguments = ["ps", "aux"]
|
|
config.stdout = ps1Buffer
|
|
}
|
|
try await psExec1.start()
|
|
var status = try await psExec1.wait()
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "ps-1 status \(status) != 0")
|
|
}
|
|
try await psExec1.delete()
|
|
|
|
guard let ps1Output = String(data: ps1Buffer.data, encoding: .utf8) else {
|
|
throw IntegrationError.assert(msg: "failed to parse ps output from container1")
|
|
}
|
|
|
|
// Verify sleep 9999 is visible in container1
|
|
guard ps1Output.contains("sleep 9999") else {
|
|
throw IntegrationError.assert(msg: "sleep 9999 should be visible in container1")
|
|
}
|
|
|
|
// List processes in container2 - should NOT see sleep 9999
|
|
let ps2Buffer = BufferWriter()
|
|
let psExec2 = try await pod.execInContainer("container2", processID: "ps-2") { config in
|
|
config.arguments = ["ps", "aux"]
|
|
config.stdout = ps2Buffer
|
|
}
|
|
try await psExec2.start()
|
|
status = try await psExec2.wait()
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "ps-2 status \(status) != 0")
|
|
}
|
|
try await psExec2.delete()
|
|
|
|
guard let ps2Output = String(data: ps2Buffer.data, encoding: .utf8) else {
|
|
throw IntegrationError.assert(msg: "failed to parse ps output from container2")
|
|
}
|
|
|
|
// Verify sleep 9999 is NOT visible in container2
|
|
guard !ps2Output.contains("sleep 9999") else {
|
|
throw IntegrationError.assert(msg: "sleep 9999 should NOT be visible in container2 (PID namespace isolation failed)")
|
|
}
|
|
|
|
try await sleepExec1.delete()
|
|
try await pod.stop()
|
|
} catch {
|
|
try? await pod.stop()
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func testPodContainerIndependentResourceLimits() async throws {
|
|
let id = "test-pod-container-independent-limits"
|
|
|
|
let bs = try await bootstrap(id)
|
|
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
|
config.cpus = 4
|
|
config.memoryInBytes = 1024.mib()
|
|
config.bootLog = bs.bootLog
|
|
}
|
|
|
|
// Container1 with 1 CPU and 128 MiB memory
|
|
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
|
|
config.process.arguments = ["/bin/sleep", "infinity"]
|
|
config.cpus = 1
|
|
config.memoryInBytes = 128.mib()
|
|
}
|
|
|
|
// Container2 with 2 CPUs and 256 MiB memory
|
|
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
|
|
config.process.arguments = ["/bin/sleep", "infinity"]
|
|
config.cpus = 2
|
|
config.memoryInBytes = 256.mib()
|
|
}
|
|
|
|
do {
|
|
try await pod.create()
|
|
try await pod.startContainer("container1")
|
|
try await pod.startContainer("container2")
|
|
|
|
// Verify container1 memory limit
|
|
let mem1Buffer = BufferWriter()
|
|
let memExec1 = try await pod.execInContainer("container1", processID: "check-mem-1") { config in
|
|
config.arguments = ["cat", "/sys/fs/cgroup/memory.max"]
|
|
config.stdout = mem1Buffer
|
|
}
|
|
try await memExec1.start()
|
|
var status = try await memExec1.wait()
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "check-mem-1 status \(status) != 0")
|
|
}
|
|
try await memExec1.delete()
|
|
|
|
guard let mem1Limit = String(data: mem1Buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
|
throw IntegrationError.assert(msg: "failed to parse memory.max from container1")
|
|
}
|
|
|
|
let expectedMem1 = "\(128.mib())"
|
|
guard mem1Limit == expectedMem1 else {
|
|
throw IntegrationError.assert(msg: "container1 memory.max \(mem1Limit) != expected \(expectedMem1)")
|
|
}
|
|
|
|
// Verify container1 CPU limit
|
|
let cpu1Buffer = BufferWriter()
|
|
let cpuExec1 = try await pod.execInContainer("container1", processID: "check-cpu-1") { config in
|
|
config.arguments = ["cat", "/sys/fs/cgroup/cpu.max"]
|
|
config.stdout = cpu1Buffer
|
|
}
|
|
try await cpuExec1.start()
|
|
status = try await cpuExec1.wait()
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "check-cpu-1 status \(status) != 0")
|
|
}
|
|
try await cpuExec1.delete()
|
|
|
|
guard let cpu1Limit = String(data: cpu1Buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
|
throw IntegrationError.assert(msg: "failed to parse cpu.max from container1")
|
|
}
|
|
|
|
let expectedCpu1 = "100000 100000" // 1 CPU
|
|
guard cpu1Limit == expectedCpu1 else {
|
|
throw IntegrationError.assert(msg: "container1 cpu.max '\(cpu1Limit)' != expected '\(expectedCpu1)'")
|
|
}
|
|
|
|
// Verify container2 memory limit
|
|
let mem2Buffer = BufferWriter()
|
|
let memExec2 = try await pod.execInContainer("container2", processID: "check-mem-2") { config in
|
|
config.arguments = ["cat", "/sys/fs/cgroup/memory.max"]
|
|
config.stdout = mem2Buffer
|
|
}
|
|
try await memExec2.start()
|
|
status = try await memExec2.wait()
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "check-mem-2 status \(status) != 0")
|
|
}
|
|
try await memExec2.delete()
|
|
|
|
guard let mem2Limit = String(data: mem2Buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
|
throw IntegrationError.assert(msg: "failed to parse memory.max from container2")
|
|
}
|
|
|
|
let expectedMem2 = "\(256.mib())"
|
|
guard mem2Limit == expectedMem2 else {
|
|
throw IntegrationError.assert(msg: "container2 memory.max \(mem2Limit) != expected \(expectedMem2)")
|
|
}
|
|
|
|
// Verify container2 CPU limit
|
|
let cpu2Buffer = BufferWriter()
|
|
let cpuExec2 = try await pod.execInContainer("container2", processID: "check-cpu-2") { config in
|
|
config.arguments = ["cat", "/sys/fs/cgroup/cpu.max"]
|
|
config.stdout = cpu2Buffer
|
|
}
|
|
try await cpuExec2.start()
|
|
status = try await cpuExec2.wait()
|
|
guard status.exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "check-cpu-2 status \(status) != 0")
|
|
}
|
|
try await cpuExec2.delete()
|
|
|
|
guard let cpu2Limit = String(data: cpu2Buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
|
throw IntegrationError.assert(msg: "failed to parse cpu.max from container2")
|
|
}
|
|
|
|
let expectedCpu2 = "200000 100000" // 2 CPUs
|
|
guard cpu2Limit == expectedCpu2 else {
|
|
throw IntegrationError.assert(msg: "container2 cpu.max '\(cpu2Limit)' != expected '\(expectedCpu2)'")
|
|
}
|
|
|
|
try await pod.stop()
|
|
} catch {
|
|
try? await pod.stop()
|
|
throw error
|
|
}
|
|
}
|
|
}
|