diff --git a/Package.resolved b/Package.resolved index c9f6288e..57654d50 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b606e718aa690122c644e34349c372f37b67ae1641c1f4e6216fad7aad555dc9", + "originHash" : "22a1e1b6903f45f7d9b7dc7940fd328f427c4123c73d6985eab99ae6c21523c4", "pins" : [ { "identity" : "async-http-client", @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "6675bc0ff86e61436e615df6fc5174e043e57924", - "version" : "1.4.1" + "revision" : "03cc312c2c933ed87abace34044a5dff7a3117c1", + "version" : "1.5.0" } }, { diff --git a/Package.swift b/Package.swift index cf3f1aa5..4323fcfe 100644 --- a/Package.swift +++ b/Package.swift @@ -108,6 +108,7 @@ let package = Package( "ContainerPersistence", "ContainerPlugin", "ContainerResource", + "ContainerRuntimeLinuxTypes", "ContainerVersion", "ContainerXPC", "TerminalProgress", @@ -199,6 +200,7 @@ let package = Package( dependencies: [ .product(name: "Containerization", package: "containerization"), "ContainerResource", + "ContainerRuntimeLinuxTypes", "ContainerSandboxServiceClient", ] ), @@ -333,6 +335,11 @@ let package = Package( ], path: "Sources/Services/ContainerNetworkService/Client" ), + .target( + name: "ContainerRuntimeLinuxTypes", + dependencies: [], + path: "Sources/Plugins/RuntimeLinux/Types" + ), .executableTarget( name: "container-runtime-linux", dependencies: [ @@ -342,13 +349,14 @@ let package = Package( "ContainerLog", "ContainerPlugin", "ContainerResource", + "ContainerRuntimeLinuxTypes", "ContainerSandboxService", "ContainerSandboxServiceClient", "ContainerVersion", "ContainerXPC", ], path: "Sources/Plugins/RuntimeLinux", - exclude: ["config.toml"] + exclude: ["config.toml", "Types"] ), .target( name: "ContainerSandboxService", diff --git a/Sources/Plugins/RuntimeLinux/Types/LinuxRuntimeData.swift b/Sources/Plugins/RuntimeLinux/Types/LinuxRuntimeData.swift new file mode 100644 index 00000000..d30185a1 --- /dev/null +++ b/Sources/Plugins/RuntimeLinux/Types/LinuxRuntimeData.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the container 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 Foundation + +/// Linux-specific runtime data passed through the opaque runtimeData field +/// in RuntimeConfiguration. Encoded by the CLI, decoded by the Linux runtime. +public struct LinuxRuntimeData: Codable, Sendable { + public let variant: String? + + public init(variant: String? = nil) { + self.variant = variant + } +} diff --git a/Sources/Services/ContainerAPIService/Client/ContainerClient.swift b/Sources/Services/ContainerAPIService/Client/ContainerClient.swift index 01a6efde..c108ef3d 100644 --- a/Sources/Services/ContainerAPIService/Client/ContainerClient.swift +++ b/Sources/Services/ContainerAPIService/Client/ContainerClient.swift @@ -49,7 +49,8 @@ public struct ContainerClient: Sendable { configuration: ContainerConfiguration, options: ContainerCreateOptions = .default, kernel: Kernel, - initImage: String? = nil + initImage: String? = nil, + runtimeData: Data? = nil ) async throws { do { let request = XPCMessage(route: .containerCreate) @@ -65,6 +66,10 @@ public struct ContainerClient: Sendable { request.set(key: .initImage, value: initImage) } + if let runtimeData { + request.set(key: .runtimeData, value: runtimeData) + } + try await xpcSend(message: request) } catch { throw ContainerizationError( diff --git a/Sources/Services/ContainerAPIService/Client/XPC+.swift b/Sources/Services/ContainerAPIService/Client/XPC+.swift index 033cfb4f..952e1a4c 100644 --- a/Sources/Services/ContainerAPIService/Client/XPC+.swift +++ b/Sources/Services/ContainerAPIService/Client/XPC+.swift @@ -32,6 +32,8 @@ public enum XPCKeys: String { case containerConfig /// Container options key. case containerOptions + /// Opaque runtime-specific data. + case runtimeData /// Vsock port number key. case port /// Exit code for a process diff --git a/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift b/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift index 522e6d8e..ad76412f 100644 --- a/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift +++ b/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift @@ -196,8 +196,9 @@ public struct ContainersHarness: Sendable { let kernel = try JSONDecoder().decode(Kernel.self, from: kdata) let initImage = message.string(key: .initImage) + let runtimeData = message.dataNoCopy(key: .runtimeData) - try await service.create(configuration: config, kernel: kernel, options: options, initImage: initImage) + try await service.create(configuration: config, kernel: kernel, options: options, initImage: initImage, runtimeData: runtimeData) return message.reply() } diff --git a/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift b/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift index 77c4e762..8bcf8095 100644 --- a/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift +++ b/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift @@ -277,7 +277,7 @@ public actor ContainersService { } /// Create a new container from the provided id and configuration. - public func create(configuration: ContainerConfiguration, kernel: Kernel, options: ContainerCreateOptions, initImage: String? = nil) async throws { + public func create(configuration: ContainerConfiguration, kernel: Kernel, options: ContainerCreateOptions, initImage: String? = nil, runtimeData: Data? = nil) async throws { log.debug( "ContainersService: enter", metadata: [ @@ -381,7 +381,8 @@ public actor ContainersService { kernel: kernel, containerConfiguration: configuration, containerRootFilesystem: imageFs, - options: options + options: options, + runtimeData: runtimeData ) try runtimeConfig.writeRuntimeConfiguration() diff --git a/Sources/Services/ContainerSandboxService/Client/SandboxRuntimeConfiguration.swift b/Sources/Services/ContainerSandboxService/Client/SandboxRuntimeConfiguration.swift index 66a04d86..5863ebf5 100644 --- a/Sources/Services/ContainerSandboxService/Client/SandboxRuntimeConfiguration.swift +++ b/Sources/Services/ContainerSandboxService/Client/SandboxRuntimeConfiguration.swift @@ -28,6 +28,7 @@ public struct RuntimeConfiguration: Codable, Sendable { public let containerConfiguration: ContainerConfiguration? public let containerRootFilesystem: Filesystem? public let options: ContainerCreateOptions? + public let runtimeData: Data? public init( path: URL, @@ -35,7 +36,8 @@ public struct RuntimeConfiguration: Codable, Sendable { kernel: Kernel, containerConfiguration: ContainerConfiguration? = nil, containerRootFilesystem: Filesystem? = nil, - options: ContainerCreateOptions? = nil + options: ContainerCreateOptions? = nil, + runtimeData: Data? = nil ) { self.path = path self.initialFilesystem = initialFilesystem @@ -43,6 +45,7 @@ public struct RuntimeConfiguration: Codable, Sendable { self.containerConfiguration = containerConfiguration self.containerRootFilesystem = containerRootFilesystem self.options = options + self.runtimeData = runtimeData } public var runtimeConfigurationPath: URL { diff --git a/Sources/Services/ContainerSandboxService/Server/SandboxService.swift b/Sources/Services/ContainerSandboxService/Server/SandboxService.swift index 1383b8b5..e65b09a4 100644 --- a/Sources/Services/ContainerSandboxService/Server/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/Server/SandboxService.swift @@ -139,6 +139,8 @@ public actor SandboxService { self.log.debug("enter", metadata: ["func": "\(#function)"]) defer { self.log.debug("exit", metadata: ["func": "\(#function)"]) } + // Create the bundle if it doesn't exist yet + // Create the bundle if it doesn't exist yet if !self.bundleExists(at: self.root) { try self.createBundle() diff --git a/Tests/ContainerAPIServiceTests/RuntimeConfigurationTests.swift b/Tests/ContainerAPIServiceTests/RuntimeConfigurationTests.swift index c54c8a79..1871e083 100644 --- a/Tests/ContainerAPIServiceTests/RuntimeConfigurationTests.swift +++ b/Tests/ContainerAPIServiceTests/RuntimeConfigurationTests.swift @@ -14,11 +14,10 @@ // limitations under the License. //===----------------------------------------------------------------------===// -// import ContainerAPIService import ContainerResource +import ContainerRuntimeLinuxTypes import ContainerSandboxServiceClient import Containerization -// import ContainerizationOCI import Foundation import Testing @@ -72,10 +71,6 @@ struct RuntimeConfigurationTests { try runtimeConfig.writeRuntimeConfiguration() - defer { - try? FileManager.default.removeItem(at: runtimeConfig.runtimeConfigurationPath) - } - let readRuntimeConfig = try RuntimeConfiguration.readRuntimeConfiguration(from: bundlePath) #expect( @@ -97,4 +92,44 @@ struct RuntimeConfigurationTests { readRuntimeConfig.options == nil, "Options should be nil") } + + @Test + func testRuntimeConfigurationWithVariant() throws { + let tempDir = FileManager.default.temporaryDirectory + let bundlePath = tempDir.appendingPathComponent("test-bundle-\(UUID())") + + defer { + try? FileManager.default.removeItem(at: bundlePath) + } + + let initFs = Filesystem.virtiofs( + source: "/path/to/initfs", + destination: "/", + options: ["ro"] + ) + + let kernel = Kernel( + path: URL(fileURLWithPath: "/path/to/kernel"), + platform: .linuxArm + ) + + let linuxData = LinuxRuntimeData(variant: "test-variant") + let encodedData = try JSONEncoder().encode(linuxData) + + let runtimeConfig = RuntimeConfiguration( + path: bundlePath, + initialFilesystem: initFs, + kernel: kernel, + runtimeData: encodedData + ) + + try runtimeConfig.writeRuntimeConfiguration() + + let readRuntimeConfig = try RuntimeConfiguration.readRuntimeConfiguration(from: bundlePath) + + #expect(readRuntimeConfig.runtimeData != nil, "runtimeData should be persisted") + + let decodedData = try JSONDecoder().decode(LinuxRuntimeData.self, from: readRuntimeConfig.runtimeData!) + #expect(decodedData.variant == "test-variant", "Variant should round-trip through RuntimeConfiguration") + } }