mirror of
https://github.com/apple/container.git
synced 2026-09-10 09:45:39 +00:00
This change adds a new private method `waitIoComplete` on the `LinuxProcess` type. This method is called internally when the user calls `wait` for a process - and it tries to give the IO streams some time to clear their buffers. Internally, this method sets up an `AsyncStream` down which an item is sent when the vsock connection for either stdout/stderr is terminated. We get this termination signal when the readability handler for the associated fd fires with a no available data. Inside the guest - once we are done relaying the IO from the process into the socket connection, we close the socket fd which triggers the above. All this logic is wrapped around a timeout of 3 seconds, just to ensure the method does not block forever. --------- Signed-off-by: Aditya Ramani <a_ramani@apple.com>
244 lines
8.0 KiB
Swift
244 lines
8.0 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
|
|
//
|
|
// 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 ContainerizationExtras
|
|
import ContainerizationOCI
|
|
import ContainerizationOS
|
|
import Foundation
|
|
import Logging
|
|
import NIOCore
|
|
|
|
let log = {
|
|
LoggingSystem.bootstrap(StreamLogHandler.standardError)
|
|
var log = Logger(label: "com.apple.containerization")
|
|
log.logLevel = .debug
|
|
return log
|
|
}()
|
|
|
|
enum IntegrationError: Swift.Error {
|
|
case assert(msg: String)
|
|
case noOutput
|
|
}
|
|
|
|
struct SkipTest: Swift.Error, CustomStringConvertible {
|
|
let reason: String
|
|
|
|
var description: String {
|
|
reason
|
|
}
|
|
}
|
|
|
|
@main
|
|
struct IntegrationSuite: AsyncParsableCommand {
|
|
static let appRoot: URL = {
|
|
FileManager.default.urls(
|
|
for: .applicationSupportDirectory,
|
|
in: .userDomainMask
|
|
).first!
|
|
.appendingPathComponent("com.apple.containerization")
|
|
}()
|
|
|
|
private static let _contentStore: ContentStore = {
|
|
try! LocalContentStore(path: appRoot.appending(path: "content"))
|
|
}()
|
|
|
|
private static let _imageStore: ImageStore = {
|
|
try! ImageStore(
|
|
path: appRoot,
|
|
contentStore: contentStore
|
|
)
|
|
}()
|
|
|
|
static let _testDir: URL = {
|
|
FileManager.default.uniqueTemporaryDirectory(create: true)
|
|
}()
|
|
|
|
static var testDir: URL {
|
|
_testDir
|
|
}
|
|
|
|
static var imageStore: ImageStore {
|
|
_imageStore
|
|
}
|
|
|
|
static var contentStore: ContentStore {
|
|
_contentStore
|
|
}
|
|
|
|
static let initImage = "vminit:latest"
|
|
|
|
@Option(name: .shortAndLong, help: "Path to a log file")
|
|
var bootlog: String
|
|
|
|
@Option(name: .shortAndLong, help: "Path to a kernel binary")
|
|
var kernel: String = "./bin/vmlinux"
|
|
|
|
static func binPath(name: String) -> URL {
|
|
URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
|
|
.appendingPathComponent("bin")
|
|
.appendingPathComponent(name)
|
|
}
|
|
|
|
func bootstrap() async throws -> (rootfs: Containerization.Mount, vmm: VirtualMachineManager) {
|
|
let reference = "ghcr.io/linuxcontainers/alpine:3.20"
|
|
let store = Self.imageStore
|
|
|
|
let initImage = try await store.getInitImage(reference: Self.initImage)
|
|
let initfs = try await {
|
|
let p = Self.binPath(name: "init.block")
|
|
do {
|
|
return try await initImage.initBlock(at: p, for: .linuxArm)
|
|
} catch let err as ContainerizationError {
|
|
guard err.code == .exists else {
|
|
throw err
|
|
}
|
|
return .block(
|
|
format: "ext4",
|
|
source: p.absolutePath(),
|
|
destination: "/",
|
|
options: ["ro"]
|
|
)
|
|
}
|
|
}()
|
|
|
|
var testKernel = Kernel(path: .init(filePath: kernel), platform: .linuxArm)
|
|
testKernel.commandLine.addDebug()
|
|
let image = try await Self.fetchImage(reference: reference, store: store)
|
|
let platform = Platform(arch: "arm64", os: "linux", variant: "v8")
|
|
|
|
let fs: Containerization.Mount = try await {
|
|
let fsPath = Self.testDir.appending(component: "rootfs.ext4")
|
|
do {
|
|
return try await image.unpack(for: platform, at: fsPath)
|
|
} catch let err as ContainerizationError {
|
|
if err.code == .exists {
|
|
return .block(
|
|
format: "ext4",
|
|
source: fsPath.absolutePath(),
|
|
destination: "/",
|
|
options: []
|
|
)
|
|
}
|
|
throw err
|
|
}
|
|
}()
|
|
|
|
let clPath = Self.testDir.appending(component: "rn.ext4").absolutePath()
|
|
try? FileManager.default.removeItem(atPath: clPath)
|
|
|
|
let cl = try fs.clone(to: clPath)
|
|
return (
|
|
cl,
|
|
VZVirtualMachineManager(
|
|
kernel: testKernel,
|
|
initialFilesystem: initfs,
|
|
bootlog: bootlog
|
|
)
|
|
)
|
|
}
|
|
|
|
static func fetchImage(reference: String, store: ImageStore) async throws -> Containerization.Image {
|
|
do {
|
|
return try await store.get(reference: reference)
|
|
} catch let error as ContainerizationError {
|
|
if error.code == .notFound {
|
|
return try await store.pull(reference: reference)
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
static func adjustLimits() throws {
|
|
var limits = rlimit()
|
|
guard getrlimit(RLIMIT_NOFILE, &limits) == 0 else {
|
|
throw POSIXError(.init(rawValue: errno)!)
|
|
}
|
|
limits.rlim_cur = 65536
|
|
limits.rlim_max = 65536
|
|
|
|
guard setrlimit(RLIMIT_NOFILE, &limits) == 0 else {
|
|
throw POSIXError(.init(rawValue: errno)!)
|
|
}
|
|
}
|
|
|
|
// Why does this exist?
|
|
//
|
|
// We need the virtualization entitlement to execute these tests.
|
|
// There currently does not exist a strightforward way to do this
|
|
// in a pure swift package.
|
|
//
|
|
// In order to not have a dependency on xcode, we create an executable
|
|
// for our integration tests that can be signed then ran.
|
|
//
|
|
// We also can't import Testing as it expects to be run from a runner.
|
|
// Hopefully this improves over time.
|
|
func run() async throws {
|
|
try Self.adjustLimits()
|
|
let suiteStarted = CFAbsoluteTimeGetCurrent()
|
|
log.info("starting integration suite\n")
|
|
|
|
let tests: [String: () async throws -> Void] = [
|
|
"process true": testProcessTrue,
|
|
"process false": testProcessFalse,
|
|
"process echo hi": testProcessEchoHi,
|
|
"process user": testProcessUser,
|
|
"process home envvar": testProcessHomeEnvvar,
|
|
"multiple concurrent processes": testMultipleConcurrentProcesses,
|
|
"multiple concurrent processes with output stress": testMultipleConcurrentProcessesOutputStress,
|
|
"container hostname": testHostname,
|
|
"container mount": testMounts,
|
|
"nested virt": testNestedVirtualizationEnabled,
|
|
]
|
|
|
|
var passed = 0
|
|
var skipped = 0
|
|
for (name, test) in tests {
|
|
do {
|
|
log.info("test \(name) started...")
|
|
|
|
let started = CFAbsoluteTimeGetCurrent()
|
|
try await test()
|
|
let lasted = CFAbsoluteTimeGetCurrent() - started
|
|
log.info("✅ test \(name) complete in \(lasted)s.")
|
|
passed += 1
|
|
} catch let err as SkipTest {
|
|
log.info("⏭️ skipped test: \(err)")
|
|
skipped += 1
|
|
} catch {
|
|
log.error("❌ test \(name) failed: \(error)")
|
|
}
|
|
}
|
|
|
|
let ended = CFAbsoluteTimeGetCurrent() - suiteStarted
|
|
var finishingText = "\n\nIntegration suite completed in \(ended)s with \(passed)/\(tests.count) passed"
|
|
if skipped > 0 {
|
|
finishingText += " and \(skipped)/\(tests.count) skipped"
|
|
}
|
|
finishingText += "!"
|
|
|
|
log.info("\(finishingText)")
|
|
|
|
if passed + skipped < tests.count {
|
|
log.error("❌")
|
|
throw ExitCode(1)
|
|
}
|
|
try? FileManager.default.removeItem(at: Self.testDir)
|
|
}
|
|
}
|