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`.
This commit is contained in:
J Logan
2026-04-20 20:38:33 -07:00
committed by GitHub
parent acfbf2d2db
commit d15fe22e31
10 changed files with 45 additions and 39 deletions
@@ -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()
@@ -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 {
@@ -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 {
@@ -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)
@@ -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
@@ -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()
}
@@ -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,
@@ -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)
@@ -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
@@ -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] {