From d15fe22e315af0a5d637efd317ef30bee54661b3 Mon Sep 17 00:00:00 2001 From: J Logan Date: Mon, 20 Apr 2026 20:38:33 -0700 Subject: [PATCH] Makes bootstrap env parameter compatible with older clients. (#1434) - Closes #1433. - Renames `env` parameter to `dynamicEnv` to differentiate these special-case environment variables from the standard container environment. - Use `[String: String]` instead of `[String: String]?`. - Default to `[:]` when an down-revision client calls bootstrap without supplying `dynamicEnv`. --- .../Builder/BuilderStart.swift | 6 ++--- .../Container/ContainerRun.swift | 6 ++--- .../Container/ContainerStart.swift | 2 +- .../Client/ContainerClient.swift | 10 +++++--- .../ContainerAPIService/Client/XPC+.swift | 4 +-- .../Server/Containers/ContainersHarness.swift | 11 +++----- .../Server/Containers/ContainersService.swift | 6 ++--- .../Client/SandboxClient.swift | 10 +++++--- .../Client/SandboxKeys.swift | 4 +-- .../Server/SandboxService.swift | 25 +++++++++++-------- 10 files changed, 45 insertions(+), 39 deletions(-) diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index e9317b75..137e1ce0 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -326,12 +326,12 @@ private func startBuildKit( ) defer { try? io.close() } - var env: [String: String] = [:] + var dynamicEnv: [String: String] = [:] if let sshAuthSock = ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] { - env["SSH_AUTH_SOCK"] = sshAuthSock + dynamicEnv["SSH_AUTH_SOCK"] = sshAuthSock } - let process = try await client.bootstrap(id: id, stdio: io.stdio, env: env) + let process = try await client.bootstrap(id: id, stdio: io.stdio, dynamicEnv: dynamicEnv) try await process.start() await taskManager?.finish() try io.closeAfterStart() diff --git a/Sources/ContainerCommands/Container/ContainerRun.swift b/Sources/ContainerCommands/Container/ContainerRun.swift index 6be43d81..0bb4d20d 100644 --- a/Sources/ContainerCommands/Container/ContainerRun.swift +++ b/Sources/ContainerCommands/Container/ContainerRun.swift @@ -123,12 +123,12 @@ extension Application { try? io.close() } - var env: [String: String] = [:] + var dynamicEnv: [String: String] = [:] if let sshAuthSock = ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] { - env["SSH_AUTH_SOCK"] = sshAuthSock + dynamicEnv["SSH_AUTH_SOCK"] = sshAuthSock } - let process = try await client.bootstrap(id: id, stdio: io.stdio, env: env) + let process = try await client.bootstrap(id: id, stdio: io.stdio, dynamicEnv: dynamicEnv) progress.finish() if !self.managementFlags.cidfile.isEmpty { diff --git a/Sources/ContainerCommands/Container/ContainerStart.swift b/Sources/ContainerCommands/Container/ContainerStart.swift index a7f96f89..00bc47a1 100644 --- a/Sources/ContainerCommands/Container/ContainerStart.swift +++ b/Sources/ContainerCommands/Container/ContainerStart.swift @@ -92,7 +92,7 @@ extension Application { env["SSH_AUTH_SOCK"] = sshAuthSock } - let process = try await client.bootstrap(id: container.id, stdio: io.stdio, env: env) + let process = try await client.bootstrap(id: container.id, stdio: io.stdio, dynamicEnv: env) progress.finish() if detach { diff --git a/Sources/Services/ContainerAPIService/Client/ContainerClient.swift b/Sources/Services/ContainerAPIService/Client/ContainerClient.swift index 33608879..01a6efde 100644 --- a/Sources/Services/ContainerAPIService/Client/ContainerClient.swift +++ b/Sources/Services/ContainerAPIService/Client/ContainerClient.swift @@ -113,7 +113,11 @@ public struct ContainerClient: Sendable { } /// Bootstrap the container's init process. - public func bootstrap(id: String, stdio: [FileHandle?], env: [String: String]? = nil) async throws -> ClientProcess { + public func bootstrap( + id: String, + stdio: [FileHandle?], + dynamicEnv: [String: String] = [:] + ) async throws -> ClientProcess { let request = XPCMessage(route: .containerBootstrap) for (i, h) in stdio.enumerated() { @@ -133,8 +137,8 @@ public struct ContainerClient: Sendable { } do { - let env = try JSONEncoder().encode(env) - request.set(key: .env, value: env) + let dynamicEnv = try JSONEncoder().encode(dynamicEnv) + request.set(key: .dynamicEnv, value: dynamicEnv) request.set(key: .id, value: id) try await xpcClient.send(request) diff --git a/Sources/Services/ContainerAPIService/Client/XPC+.swift b/Sources/Services/ContainerAPIService/Client/XPC+.swift index 03feec41..9ddbaafe 100644 --- a/Sources/Services/ContainerAPIService/Client/XPC+.swift +++ b/Sources/Services/ContainerAPIService/Client/XPC+.swift @@ -56,8 +56,8 @@ public enum XPCKeys: String { case plugin /// Archive path to export rootfs case archive - /// Environment variables passed from terminal - case env + /// Special-case environment variables recomputed on each container start + case dynamicEnv /// Health check request. case ping diff --git a/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift b/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift index 15f708c3..522e6d8e 100644 --- a/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift +++ b/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift @@ -56,15 +56,10 @@ public struct ContainersHarness: Sendable { } let stdio = message.stdio() - guard let data = message.dataNoCopy(key: .env) else { - throw ContainerizationError( - .invalidArgument, - message: "env cannot be empty" - ) - } - let env = try JSONDecoder().decode([String: String]?.self, from: data) + let data = message.dataNoCopy(key: .dynamicEnv) + let env = try data.map { try JSONDecoder().decode([String: String].self, from: $0) } ?? [:] - try await service.bootstrap(id: id, stdio: stdio, env: env) + try await service.bootstrap(id: id, stdio: stdio, dynamicEnv: env) return message.reply() } diff --git a/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift b/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift index 61240e12..2c401f3b 100644 --- a/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift +++ b/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift @@ -398,13 +398,13 @@ public actor ContainersService { } /// Bootstrap the init process of the container. - public func bootstrap(id: String, stdio: [FileHandle?], env: [String: String]?) async throws { + public func bootstrap(id: String, stdio: [FileHandle?], dynamicEnv: [String: String]) async throws { log.debug( "ContainersService: enter", metadata: [ "func": "\(#function)", "id": "\(id)", - "env": "\(env ?? [:])", + "env": "\(dynamicEnv)", ] ) defer { @@ -474,7 +474,7 @@ public actor ContainersService { id: id, runtime: runtime ) - try await sandboxClient.bootstrap(stdio: stdio, allocatedAttachments: allocatedAttachments, env: env) + try await sandboxClient.bootstrap(stdio: stdio, allocatedAttachments: allocatedAttachments, dynamicEnv: dynamicEnv) try await self.exitMonitor.registerProcess( id: id, diff --git a/Sources/Services/ContainerSandboxService/Client/SandboxClient.swift b/Sources/Services/ContainerSandboxService/Client/SandboxClient.swift index 830c348f..f7bdfdad 100644 --- a/Sources/Services/ContainerSandboxService/Client/SandboxClient.swift +++ b/Sources/Services/ContainerSandboxService/Client/SandboxClient.swift @@ -77,7 +77,11 @@ public struct SandboxClient: Sendable { // Runtime Methods extension SandboxClient { - public func bootstrap(stdio: [FileHandle?], allocatedAttachments: [AllocatedAttachment], env: [String: String]? = nil) async throws { + public func bootstrap( + stdio: [FileHandle?], + allocatedAttachments: [AllocatedAttachment], + dynamicEnv: [String: String] = [:] + ) async throws { let request = XPCMessage(route: SandboxRoutes.bootstrap.rawValue) for (i, h) in stdio.enumerated() { @@ -97,8 +101,8 @@ extension SandboxClient { } do { - let env = try JSONEncoder().encode(env) - request.set(key: SandboxKeys.env.rawValue, value: env) + let dynamicEnv = try JSONEncoder().encode(dynamicEnv) + request.set(key: SandboxKeys.dynamicEnv.rawValue, value: dynamicEnv) try request.setAllocatedAttachments(allocatedAttachments) try await self.client.send(request) diff --git a/Sources/Services/ContainerSandboxService/Client/SandboxKeys.swift b/Sources/Services/ContainerSandboxService/Client/SandboxKeys.swift index 6172e0e2..cdf5ae1f 100644 --- a/Sources/Services/ContainerSandboxService/Client/SandboxKeys.swift +++ b/Sources/Services/ContainerSandboxService/Client/SandboxKeys.swift @@ -43,8 +43,8 @@ public enum SandboxKeys: String { /// Container statistics case statistics - /// Environment variables passed from terminal. - case env + /// Special-case environment variables recomputed on each container start + case dynamicEnv /// Network resource keys. case allocatedAttachments diff --git a/Sources/Services/ContainerSandboxService/Server/SandboxService.swift b/Sources/Services/ContainerSandboxService/Server/SandboxService.swift index aaa4072e..2dd0c0f8 100644 --- a/Sources/Services/ContainerSandboxService/Server/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/Server/SandboxService.swift @@ -76,12 +76,16 @@ public actor SandboxService { } } - private static func sshAuthSocketHostUrl(config: ContainerConfiguration, hostEnv: [String: String]?, log: Logger? = nil) -> URL? { + private static func sshAuthSocketHostUrl( + config: ContainerConfiguration, + dynamicEnv: [String: String] = [:], + log: Logger? = nil + ) -> URL? { guard config.ssh else { return nil } - guard let sshSocket = hostEnv?[Self.sshAuthSocketEnvVar] else { + guard let sshSocket = dynamicEnv[Self.sshAuthSocketEnvVar] else { log?.warning("ssh forwarding requested but no \(Self.sshAuthSocketEnvVar) found") return nil } @@ -147,7 +151,7 @@ public actor SandboxService { ) } - let env = try message.env() + let dynamicEnv = try message.dynamicEnv() let bundle = ContainerResource.Bundle(path: self.root) try bundle.createLogFile() @@ -224,7 +228,7 @@ public actor SandboxService { let id = config.id let rootfs = try bundle.containerRootfs.asMount let container = try LinuxContainer(id, rootfs: rootfs, vmm: vmm, logger: self.log) { czConfig in - try Self.configureContainer(czConfig: &czConfig, config: config, hostEnv: env, log: self.log) + try Self.configureContainer(czConfig: &czConfig, config: config, dynamicEnv: dynamicEnv, log: self.log) czConfig.interfaces = interfaces czConfig.process.stdout = stdout czConfig.process.stderr = stderr @@ -846,7 +850,7 @@ public actor SandboxService { private static func configureContainer( czConfig: inout LinuxContainer.Configuration, config: ContainerConfiguration, - hostEnv: [String: String]?, + dynamicEnv: [String: String] = [:], log: Logger? = nil, ) throws { czConfig.cpus = config.resources.cpus @@ -880,7 +884,7 @@ public actor SandboxService { czConfig.sockets.append(socketConfig) } - if let socketUrl = Self.sshAuthSocketHostUrl(config: config, hostEnv: hostEnv, log: log) { + if let socketUrl = Self.sshAuthSocketHostUrl(config: config, dynamicEnv: dynamicEnv, log: log) { let socketPath = socketUrl.path(percentEncoded: false) let attrs = try? FileManager.default.attributesOfItem(atPath: socketPath) let permissions = (attrs?[.posixPermissions] as? NSNumber) @@ -1165,11 +1169,10 @@ extension XPCMessage { return try JSONDecoder().decode(ProcessConfiguration.self, from: data) } - fileprivate func env() throws -> [String: String]? { - guard let data = self.dataNoCopy(key: SandboxKeys.env.rawValue) else { - throw ContainerizationError(.invalidArgument, message: "empty env") - } - return try JSONDecoder().decode([String: String]?.self, from: data) + fileprivate func dynamicEnv() throws -> [String: String] { + let data = self.dataNoCopy(key: SandboxKeys.dynamicEnv.rawValue) + let dynamicEnv = try data.map { try JSONDecoder().decode([String: String].self, from: $0) } ?? [:] + return dynamicEnv } fileprivate func getAllocatedAttachments() throws -> [AllocatedAttachment] {