Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ integration: init-block
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunBase || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIBuildBase || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIVolumes || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIKernelSet || exit_code=1 ; \
echo Ensuring apiserver stopped after the CLI integration tests ; \
scripts/ensure-container-stopped.sh ; \
exit $${exit_code} ; \
Expand Down
10 changes: 8 additions & 2 deletions Sources/APIServer/Kernel/KernelHarness.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,20 @@ struct KernelHarness {
public func install(_ message: XPCMessage) async throws -> XPCMessage {
let kernelFilePath = try message.kernelFilePath()
let platform = try message.platform()
let force = try message.kernelForce()

guard let kernelTarUrl = try message.kernelTarURL() else {
// We have been given a path to a kernel binary on disk
guard let kernelFile = URL(string: kernelFilePath) else {
throw ContainerizationError(.invalidArgument, message: "Invalid kernel file path: \(kernelFilePath)")
}
try await self.service.installKernel(kernelFile: kernelFile, platform: platform)
try await self.service.installKernel(kernelFile: kernelFile, platform: platform, force: force)
return message.reply()
}

let progressUpdateService = ProgressUpdateService(message: message)
try await self.service.installKernelFrom(tar: kernelTarUrl, kernelFilePath: kernelFilePath, platform: platform, progressUpdate: progressUpdateService?.handler)
try await self.service.installKernelFrom(
tar: kernelTarUrl, kernelFilePath: kernelFilePath, platform: platform, progressUpdate: progressUpdateService?.handler, force: force)
return message.reply()
}

Expand Down Expand Up @@ -86,4 +88,8 @@ extension XPCMessage {
}
return k
}

fileprivate func kernelForce() throws -> Bool {
self.bool(key: .kernelForce)
}
}
17 changes: 13 additions & 4 deletions Sources/APIServer/Kernel/KernelService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,19 @@ actor KernelService {

/// Copies a kernel binary from a local path on disk into the managed kernels directory
/// as the default kernel for the provided platform.
public func installKernel(kernelFile url: URL, platform: SystemPlatform = .linuxArm) throws {
public func installKernel(kernelFile url: URL, platform: SystemPlatform = .linuxArm, force: Bool) throws {
self.log.info("KernelService: \(#function) - kernelFile: \(url), platform: \(String(describing: platform))")
let kFile = url.resolvingSymlinksInPath()
let destPath = self.kernelDirectory.appendingPathComponent(kFile.lastPathComponent)
if force {
do {
try FileManager.default.removeItem(at: destPath)
} catch let error as NSError {
guard error.code == NSFileNoSuchFileError else {
throw error
}
}
}
try FileManager.default.copyItem(at: kFile, to: destPath)
try Task.checkCancellation()
do {
Expand All @@ -54,7 +63,7 @@ actor KernelService {
/// Copies a kernel binary from inside of tar file into the managed kernels directory
/// as the default kernel for the provided platform.
/// The parameter `tar` maybe a location to a local file on disk, or a remote URL.
public func installKernelFrom(tar: URL, kernelFilePath: String, platform: SystemPlatform, progressUpdate: ProgressUpdateHandler?) async throws {
public func installKernelFrom(tar: URL, kernelFilePath: String, platform: SystemPlatform, progressUpdate: ProgressUpdateHandler?, force: Bool) async throws {
self.log.info("KernelService: \(#function) - tar: \(tar), kernelFilePath: \(kernelFilePath), platform: \(String(describing: platform))")

let tempDir = FileManager.default.uniqueTemporaryDirectory()
Expand All @@ -75,15 +84,15 @@ actor KernelService {
if let progressUpdate {
downloadProgressUpdate = ProgressTaskCoordinator.handler(for: downloadTask, from: progressUpdate)
}
try await FileDownloader.downloadFile(url: tar, to: tarFile, progressUpdate: downloadProgressUpdate)
try await ContainerClient.FileDownloader.downloadFile(url: tar, to: tarFile, progressUpdate: downloadProgressUpdate)
}
await taskManager.finish()

await progressUpdate?([
.setDescription("Unpacking kernel")
])
let kernelFile = try self.extractFile(tarFile: tarFile, at: kernelFilePath, to: tempDir)
try self.installKernel(kernelFile: kernelFile, platform: platform)
try self.installKernel(kernelFile: kernelFile, platform: platform, force: force)

if !FileManager.default.fileExists(atPath: tar.absoluteString) {
try FileManager.default.removeItem(at: tarFile)
Expand Down
15 changes: 9 additions & 6 deletions Sources/CLI/System/Kernel/KernelSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,15 @@ extension Application {
@Flag(name: .customLong("recommended"), help: "Download and install the recommended kernel as the default. This flag ignores any other arguments")
var recommended: Bool = false

@Flag(name: .long, help: "Force install of kernel. If a kernel exists with the same name, it will be overwritten.")
var force: Bool = false

func run() async throws {
if recommended {
let url = DefaultsStore.get(key: .defaultKernelURL)
let path = DefaultsStore.get(key: .defaultKernelBinaryPath)
print("Installing the recommended kernel from \(url)...")
try await Self.downloadAndInstallWithProgressBar(tarRemoteURL: url, kernelFilePath: path)
try await Self.downloadAndInstallWithProgressBar(tarRemoteURL: url, kernelFilePath: path, force: force)
return
}
guard tarPath != nil else {
Expand All @@ -63,7 +66,7 @@ extension Application {
}
let absolutePath = URL(fileURLWithPath: binaryPath, relativeTo: .currentDirectory()).absoluteURL.absoluteString
let platform = try getSystemPlatform()
try await ClientKernel.installKernel(kernelFilePath: absolutePath, platform: platform)
try await ClientKernel.installKernel(kernelFilePath: absolutePath, platform: platform, force: force)
}

private func setKernelFromTar() async throws {
Expand All @@ -77,13 +80,13 @@ extension Application {
let localTarPath = URL(fileURLWithPath: tarPath, relativeTo: .currentDirectory()).absoluteString
let fm = FileManager.default
if fm.fileExists(atPath: localTarPath) {
try await ClientKernel.installKernelFromTar(tarFile: localTarPath, kernelFilePath: binaryPath, platform: platform)
try await ClientKernel.installKernelFromTar(tarFile: localTarPath, kernelFilePath: binaryPath, platform: platform, force: force)
return
}
guard let remoteURL = URL(string: tarPath) else {
throw ContainerizationError(.invalidArgument, message: "Invalid remote URL '\(tarPath)' for argument '--tar'. Missing protocol?")
}
try await Self.downloadAndInstallWithProgressBar(tarRemoteURL: remoteURL.absoluteString, kernelFilePath: binaryPath, platform: platform)
try await Self.downloadAndInstallWithProgressBar(tarRemoteURL: remoteURL.absoluteString, kernelFilePath: binaryPath, platform: platform, force: force)
}

private func getSystemPlatform() throws -> SystemPlatform {
Expand All @@ -97,7 +100,7 @@ extension Application {
}
}

public static func downloadAndInstallWithProgressBar(tarRemoteURL: String, kernelFilePath: String, platform: SystemPlatform = .current) async throws {
public static func downloadAndInstallWithProgressBar(tarRemoteURL: String, kernelFilePath: String, platform: SystemPlatform = .current, force: Bool) async throws {
let progressConfig = try ProgressConfig(
showTasks: true,
totalTasks: 2
Expand All @@ -107,7 +110,7 @@ extension Application {
progress.finish()
}
progress.start()
try await ClientKernel.installKernelFromTar(tarFile: tarRemoteURL, kernelFilePath: kernelFilePath, platform: platform, progressUpdate: progress.handler)
try await ClientKernel.installKernelFromTar(tarFile: tarRemoteURL, kernelFilePath: kernelFilePath, platform: platform, progressUpdate: progress.handler, force: force)
progress.finish()
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/CLI/System/SystemStart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ extension Application {
return
}
print("Installing kernel...")
try await KernelSet.downloadAndInstallWithProgressBar(tarRemoteURL: defaultKernelURL, kernelFilePath: defaultKernelBinaryPath)
try await KernelSet.downloadAndInstallWithProgressBar(tarRemoteURL: defaultKernelURL, kernelFilePath: defaultKernelBinaryPath, force: true)
}

private func initImageExists() async -> Bool {
Expand Down
8 changes: 6 additions & 2 deletions Sources/ContainerClient/Core/ClientKernel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,27 @@ extension ClientKernel {
XPCClient(service: serviceIdentifier)
}

public static func installKernel(kernelFilePath: String, platform: SystemPlatform) async throws {
public static func installKernel(kernelFilePath: String, platform: SystemPlatform, force: Bool) async throws {
let client = newClient()
let message = XPCMessage(route: .installKernel)

message.set(key: .kernelFilePath, value: kernelFilePath)
message.set(key: .kernelForce, value: force)

let platformData = try JSONEncoder().encode(platform)
message.set(key: .systemPlatform, value: platformData)
try await client.send(message)
}

public static func installKernelFromTar(tarFile: String, kernelFilePath: String, platform: SystemPlatform, progressUpdate: ProgressUpdateHandler? = nil) async throws {
public static func installKernelFromTar(tarFile: String, kernelFilePath: String, platform: SystemPlatform, progressUpdate: ProgressUpdateHandler? = nil, force: Bool)
async throws
{
let client = newClient()
let message = XPCMessage(route: .installKernel)

message.set(key: .kernelTarURL, value: tarFile)
message.set(key: .kernelFilePath, value: kernelFilePath)
message.set(key: .kernelForce, value: force)

let platformData = try JSONEncoder().encode(platform)
message.set(key: .systemPlatform, value: platformData)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import ContainerizationExtras
import Foundation
import TerminalProgress

internal struct FileDownloader {
public struct FileDownloader {
public static func downloadFile(url: URL, to destination: URL, progressUpdate: ProgressUpdateHandler? = nil) async throws {
let request = try HTTPClient.Request(url: url)

Expand Down
1 change: 1 addition & 0 deletions Sources/ContainerClient/XPC+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public enum XPCKeys: String {
case kernelTarURL
case kernelFilePath
case systemPlatform
case kernelForce

/// Volume
case volume
Expand Down
128 changes: 128 additions & 0 deletions Tests/CLITests/Subcommands/System/TestKernelSet.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container 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 ContainerClient
import ContainerPersistence
import ContainerizationArchive
import Foundation
import Testing

// This suite is run serialized since each test modifies the global default kernel
@Suite(.serialized)
class TestCLIKernelSet: CLITest {
let defaultKernelTar = DefaultsStore.get(key: .defaultKernelURL)
var remoteTar: URL! {
URL(string: defaultKernelTar)
}
let defaultBinaryPath = DefaultsStore.get(key: .defaultKernelBinaryPath)

deinit {
try? resetDefaultBinary()
}

func resetDefaultBinary() throws {
let arguments: [String] = [
"system",
"kernel",
"set",
"--recommended",
"--force",
]
let (_, error, status) = try run(arguments: arguments)
if status != 0 {
throw CLIError.executionFailed("failed to reset kernel to recommended: \(error)")
}
}

func doKernelSet(extraArgs: [String]) throws {
var arguments = [
"system",
"kernel",
"set",
"--force",
]
arguments.append(contentsOf: extraArgs)

let (_, error, status) = try run(arguments: arguments)
if status != 0 {
throw CLIError.executionFailed("failed to set kernel: \(error)")
}
}

func validateContainerRun() throws {
let name: String! = Test.current?.name.trimmingCharacters(in: ["(", ")"])
try doLongRun(name: name, args: [])
defer { try? doStop(name: name) }

_ = try doExec(name: name, cmd: ["date"])
try doStop(name: name)
}

@Test func fromLocalTar() async throws {
let symlinkBinaryPath: String = URL(filePath: defaultBinaryPath).deletingLastPathComponent().appending(path: "vmlinux.container").relativePath

try await withTempDir { tempDir in
// manually download the tar file
let localTarPath = tempDir.appending(path: remoteTar.lastPathComponent)
try await ContainerClient.FileDownloader.downloadFile(url: remoteTar, to: localTarPath)

let extraArgs: [String] = [
"--tar",
localTarPath.path,
"--binary",
symlinkBinaryPath,
]

try doKernelSet(extraArgs: extraArgs)
try validateContainerRun()
}
}

@Test func fromRemoteTarSymlink() throws {
// opt/kata/share/kata-containers/vmlinux.container should point to opt/kata/share/kata-containers/vmlinux-<version> in the archive
let symlinkBinaryPath: String = URL(filePath: defaultBinaryPath).deletingLastPathComponent().appending(path: "vmlinux.container").relativePath
let extraArgs: [String] = [
"--tar",
defaultKernelTar,
"--binary",
symlinkBinaryPath,
]

try doKernelSet(extraArgs: extraArgs)
try validateContainerRun()
}

@Test func fromLocalDisk() async throws {
try await withTempDir { tempDir in
// manually download the tar file
let localTarPath = tempDir.appending(path: remoteTar.lastPathComponent)
try await ContainerClient.FileDownloader.downloadFile(url: remoteTar, to: localTarPath)

// extract just the file we want
let targetPath = tempDir.appending(path: URL(string: defaultBinaryPath)!.lastPathComponent)
let archiveReader = try ArchiveReader(file: localTarPath)
let (_, data) = try archiveReader.extractFile(path: defaultBinaryPath)
try data.write(to: targetPath, options: .atomic)

let extraArgs = [
"--binary",
targetPath.path,
]
try doKernelSet(extraArgs: extraArgs)
try validateContainerRun()
}
}
}
11 changes: 11 additions & 0 deletions Tests/CLITests/Utilities/CLITest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -449,4 +449,15 @@ class CLITest {
httpConfiguration.proxy = proxyConfig
return HTTPClient(eventLoopGroupProvider: .singleton, configuration: httpConfiguration)
}

func withTempDir<T>(_ body: (URL) async throws -> T) async throws -> T {
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)

defer {
try? FileManager.default.removeItem(at: tempDir)
}

return try await body(tempDir)
}
}