Skip to content

Commit 1942a39

Browse files
authored
Minimal runtime variant plumbing. (#1239)
- Adds plugin variant selection to bootstrap() on the Container API service in such a way that we can revert the change soon without compatibility issues when we work out a more permanent approach, and requires no persistent data migration. - Restore lexical ordering on ManagementFlags (except `--runtime`, will take care of that next PR). - Add a couple plugin loader tests to improve coverage.
1 parent c48ed09 commit 1942a39

File tree

6 files changed

+103
-10
lines changed

6 files changed

+103
-10
lines changed

Sources/Services/ContainerAPIService/Client/ContainerClient.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public struct ContainerClient: Sendable {
113113
}
114114

115115
/// Bootstrap the container's init process.
116-
public func bootstrap(id: String, stdio: [FileHandle?]) async throws -> ClientProcess {
116+
public func bootstrap(id: String, stdio: [FileHandle?], variant: String? = nil) async throws -> ClientProcess {
117117
let request = XPCMessage(route: .containerBootstrap)
118118

119119
for (i, h) in stdio.enumerated() {
@@ -132,6 +132,10 @@ public struct ContainerClient: Sendable {
132132
}
133133
}
134134

135+
if let variant {
136+
request.set(key: .variant, value: variant)
137+
}
138+
135139
do {
136140
request.set(key: .id, value: id)
137141
try await xpcClient.send(request)

Sources/Services/ContainerAPIService/Client/Flags.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,9 @@ public struct Flags {
293293
)
294294
public var publishSockets: [String] = []
295295

296+
@Flag(name: .long, help: "Mount the container's root filesystem as read-only")
297+
public var readOnly = false
298+
296299
@Flag(name: [.customLong("rm"), .long], help: "Remove the container after it stops")
297300
public var remove = false
298301

@@ -305,18 +308,15 @@ public struct Flags {
305308
@Option(name: .customLong("tmpfs"), help: "Add a tmpfs mount to the container at the given path")
306309
public var tmpFs: [String] = []
307310

308-
@Option(name: [.customLong("volume"), .short], help: "Bind mount a volume into the container")
309-
public var volumes: [String] = []
310-
311311
@Flag(
312312
name: .long,
313313
help:
314314
"Expose virtualization capabilities to the container (requires host and guest support)"
315315
)
316316
public var virtualization: Bool = false
317317

318-
@Flag(name: .long, help: "Mount the container's root filesystem as read-only")
319-
public var readOnly = false
318+
@Option(name: [.customLong("volume"), .short], help: "Bind mount a volume into the container")
319+
public var volumes: [String] = []
320320

321321
@Option(name: .long, help: "Set the runtime handler for the container (default: container-runtime-linux)")
322322
public var runtime: String?

Sources/Services/ContainerAPIService/Client/XPC+.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public enum XPCKeys: String {
3232
case containerConfig
3333
/// Container options key.
3434
case containerOptions
35+
/// Plugin variant key.
36+
case variant
3537
/// Vsock port number key.
3638
case port
3739
/// Exit code for a process

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ public struct ContainersHarness: Sendable {
5555
)
5656
}
5757
let stdio = message.stdio()
58-
try await service.bootstrap(id: id, stdio: stdio)
58+
let variant = message.variant()
59+
try await service.bootstrap(id: id, stdio: stdio, variant: variant)
5960
return message.reply()
6061
}
6162

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ public actor ContainersService {
378378
}
379379

380380
/// Bootstrap the init process of the container.
381-
public func bootstrap(id: String, stdio: [FileHandle?]) async throws {
381+
public func bootstrap(id: String, stdio: [FileHandle?], variant: String? = nil) async throws {
382382
log.debug(
383383
"ContainersService: enter",
384384
metadata: [
@@ -424,8 +424,8 @@ public actor ContainersService {
424424
}
425425

426426
try Self.registerService(
427-
plugin: self.runtimePlugins.first { $0.name == config.runtimeHandler }!,
428427
loader: self.pluginLoader,
428+
plugin: self.runtimePlugins.first { $0.name == config.runtimeHandler }!,
429429
configuration: config,
430430
path: path,
431431
debug: self.debugHelpers
@@ -1078,14 +1078,17 @@ public actor ContainersService {
10781078
}
10791079

10801080
private static func registerService(
1081-
plugin: Plugin,
10821081
loader: PluginLoader,
1082+
plugin: Plugin,
1083+
variant: String? = nil,
10831084
configuration: ContainerConfiguration,
10841085
path: URL,
10851086
debug: Bool
10861087
) throws {
10871088
let args = [
10881089
"start",
1090+
variant != nil ? "--variant" : nil,
1091+
variant,
10891092
"--root", path.path,
10901093
"--uuid", configuration.id,
10911094
debug ? "--debug" : nil,
@@ -1179,4 +1182,8 @@ extension XPCMessage {
11791182
}
11801183
return try JSONDecoder().decode(ProcessConfiguration.self, from: data)
11811184
}
1185+
1186+
func variant() -> String? {
1187+
self.string(key: .variant)
1188+
}
11821189
}

Tests/ContainerPluginTests/PluginLoaderTest.swift

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,85 @@ struct PluginLoaderTest {
176176
#expect(filtered.isEmpty)
177177
}
178178

179+
@Test
180+
func testRegisterWithLaunchdDefaultArgs() async throws {
181+
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
182+
defer { try? FileManager.default.removeItem(at: tempURL) }
183+
let factory = try setupMock(tempURL: tempURL)
184+
let loader = try PluginLoader(
185+
appRoot: tempURL,
186+
installRoot: URL(filePath: "/usr/local/"),
187+
logRoot: nil,
188+
pluginDirectories: [tempURL],
189+
pluginFactories: [factory]
190+
)
191+
192+
let plugin = loader.findPlugin(name: "service")!
193+
let stateRoot = tempURL.appendingPathComponent("test-state")
194+
try loader.registerWithLaunchd(plugin: plugin, pluginStateRoot: stateRoot)
195+
196+
let plistURL = stateRoot.appendingPathComponent("service.plist")
197+
let plistData = try Data(contentsOf: plistURL)
198+
let plist = try PropertyListSerialization.propertyList(from: plistData, format: nil) as! [String: Any]
199+
let programArguments = plist["ProgramArguments"] as! [String]
200+
201+
#expect(programArguments.contains("start"))
202+
}
203+
204+
@Test
205+
func testRegisterWithLaunchdCustomArgs() async throws {
206+
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
207+
defer { try? FileManager.default.removeItem(at: tempURL) }
208+
let factory = try setupMock(tempURL: tempURL)
209+
let loader = try PluginLoader(
210+
appRoot: tempURL,
211+
installRoot: URL(filePath: "/usr/local/"),
212+
logRoot: nil,
213+
pluginDirectories: [tempURL],
214+
pluginFactories: [factory]
215+
)
216+
217+
let plugin = loader.findPlugin(name: "service")!
218+
let stateRoot = tempURL.appendingPathComponent("test-state")
219+
try loader.registerWithLaunchd(plugin: plugin, pluginStateRoot: stateRoot, args: ["run", "--verbose"])
220+
221+
let plistURL = stateRoot.appendingPathComponent("service.plist")
222+
let plistData = try Data(contentsOf: plistURL)
223+
let plist = try PropertyListSerialization.propertyList(from: plistData, format: nil) as! [String: Any]
224+
let programArguments = plist["ProgramArguments"] as! [String]
225+
226+
#expect(programArguments.contains("run"))
227+
#expect(programArguments.contains("--verbose"))
228+
#expect(!programArguments.contains("start"))
229+
}
230+
231+
@Test
232+
func testRegisterWithLaunchdCustomArgsAndDebug() async throws {
233+
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
234+
defer { try? FileManager.default.removeItem(at: tempURL) }
235+
let factory = try setupMock(tempURL: tempURL)
236+
let loader = try PluginLoader(
237+
appRoot: tempURL,
238+
installRoot: URL(filePath: "/usr/local/"),
239+
logRoot: nil,
240+
pluginDirectories: [tempURL],
241+
pluginFactories: [factory]
242+
)
243+
244+
let plugin = loader.findPlugin(name: "service")!
245+
let stateRoot = tempURL.appendingPathComponent("test-state")
246+
try loader.registerWithLaunchd(plugin: plugin, pluginStateRoot: stateRoot, args: ["run"], debug: true)
247+
248+
let plistURL = stateRoot.appendingPathComponent("service.plist")
249+
let plistData = try Data(contentsOf: plistURL)
250+
let plist = try PropertyListSerialization.propertyList(from: plistData, format: nil) as! [String: Any]
251+
let programArguments = plist["ProgramArguments"] as! [String]
252+
253+
#expect(programArguments.contains("run"))
254+
#expect(programArguments.contains("--debug"))
255+
#expect(!programArguments.contains("start"))
256+
}
257+
179258
@Test
180259
func testRegisterWithLaunchdDebugTrue() async throws {
181260
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)

0 commit comments

Comments
 (0)