Skip to content

Commit c396115

Browse files
committed
General bootstrap env that overrides with dynamicEnv for SSH forwarding
1 parent a9f33a9 commit c396115

12 files changed

Lines changed: 189 additions & 29 deletions

File tree

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,8 @@ let package = Package(
354354
.product(name: "Containerization", package: "containerization"),
355355
.product(name: "ContainerizationExtras", package: "containerization"),
356356
.product(name: "ContainerizationOCI", package: "containerization"),
357-
"ContainerAPIService",
358357
"ContainerResource",
358+
"ContainerAPIService",
359359
]
360360
),
361361
.target(
@@ -394,6 +394,7 @@ let package = Package(
394394
dependencies: [
395395
.product(name: "Containerization", package: "containerization"),
396396
"ContainerResource",
397+
"ContainerSandboxService",
397398
"ContainerSandboxServiceClient",
398399
]
399400
),

Sources/ContainerCommands/Container/ContainerRun.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,11 @@ extension Application {
128128
try? io.close()
129129
}
130130

131-
let sshAuthSocketPath = ck.0.ssh ? ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] : nil
132-
let process = try await client.bootstrap(id: id, stdio: io.stdio, sshAuthSocketPath: sshAuthSocketPath)
131+
var dynamicEnv: [String: String] = [:]
132+
if ck.0.ssh, let sshAuthSock = ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] {
133+
dynamicEnv["SSH_AUTH_SOCK"] = sshAuthSock
134+
}
135+
let process = try await client.bootstrap(id: id, stdio: io.stdio, dynamicEnv: dynamicEnv)
133136
progress.finish()
134137

135138
if !self.managementFlags.cidfile.isEmpty {

Sources/ContainerCommands/Container/ContainerStart.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,11 @@ extension Application {
8787
try? io.close()
8888
}
8989

90-
let sshAuthSocketPath = container.configuration.ssh ? ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] : nil
91-
let process = try await client.bootstrap(id: container.id, stdio: io.stdio, sshAuthSocketPath: sshAuthSocketPath)
90+
var dynamicEnv: [String: String] = [:]
91+
if container.configuration.ssh, let sshAuthSock = ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] {
92+
dynamicEnv["SSH_AUTH_SOCK"] = sshAuthSock
93+
}
94+
let process = try await client.bootstrap(id: container.id, stdio: io.stdio, dynamicEnv: dynamicEnv)
9295
progress.finish()
9396

9497
if detach {

Sources/Services/ContainerAPIService/Client/ContainerClient.swift

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

115115
/// Bootstrap the container's init process.
116-
/// - Parameter sshAuthSocketPath: Optional path to the current shell's SSH agent socket, supplied at bootstrap time when SSH forwarding is enabled.
117-
public func bootstrap(id: String, stdio: [FileHandle?], sshAuthSocketPath: String? = nil) async throws -> ClientProcess {
116+
/// - Parameter dynamicEnv: Optional start-time environment overrides passed through bootstrap.
117+
public func bootstrap(id: String, stdio: [FileHandle?], dynamicEnv: [String: String] = [:]) async throws -> ClientProcess {
118118
let request = XPCMessage(route: .containerBootstrap)
119119

120120
for (i, h) in stdio.enumerated() {
@@ -135,8 +135,9 @@ public struct ContainerClient: Sendable {
135135

136136
do {
137137
request.set(key: .id, value: id)
138-
if let sshAuthSocketPath {
139-
request.set(key: .sshAuthSocketPath, value: sshAuthSocketPath)
138+
if !dynamicEnv.isEmpty {
139+
let encodedDynamicEnv = try JSONEncoder().encode(dynamicEnv)
140+
request.set(key: .dynamicEnv, value: encodedDynamicEnv)
140141
}
141142
try await xpcClient.send(request)
142143
return ClientProcessImpl(containerId: id, xpcClient: xpcClient)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public enum XPCKeys: String {
113113
case initImage
114114

115115
/// SSH agent socket path supplied at bootstrap time (current client shell).
116-
case sshAuthSocketPath
116+
case dynamicEnv
117117

118118
/// Volume
119119
case volume

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,13 @@ public struct ContainersHarness: Sendable {
5555
)
5656
}
5757
let stdio = message.stdio()
58-
let sshAuthSocketPath = message.string(key: .sshAuthSocketPath)
59-
try await service.bootstrap(id: id, stdio: stdio, sshAuthSocketPath: sshAuthSocketPath)
58+
let dynamicEnv: [String: String] =
59+
if let data = message.dataNoCopy(key: .dynamicEnv) {
60+
try JSONDecoder().decode([String: String].self, from: data)
61+
} else {
62+
[:]
63+
}
64+
try await service.bootstrap(id: id, stdio: stdio, dynamicEnv: dynamicEnv)
6065
return message.reply()
6166
}
6267

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,8 @@ public actor ContainersService {
398398
}
399399

400400
/// Bootstrap the init process of the container.
401-
public func bootstrap(id: String, stdio: [FileHandle?]) async throws {
401+
/// - Parameter dynamicEnv: Optional start-time environment overrides passed from the client.
402+
public func bootstrap(id: String, stdio: [FileHandle?], dynamicEnv: [String: String] = [:]) async throws {
402403
log.debug(
403404
"ContainersService: enter",
404405
metadata: [
@@ -473,7 +474,7 @@ public actor ContainersService {
473474
id: id,
474475
runtime: runtime
475476
)
476-
try await sandboxClient.bootstrap(stdio: stdio, allocatedAttachments: allocatedAttachments)
477+
try await sandboxClient.bootstrap(stdio: stdio, allocatedAttachments: allocatedAttachments, dynamicEnv: dynamicEnv)
477478

478479
try await self.exitMonitor.registerProcess(
479480
id: id,

Sources/Services/ContainerSandboxService/Client/SandboxClient.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ public struct SandboxClient: Sendable {
7777

7878
// Runtime Methods
7979
extension SandboxClient {
80-
public func bootstrap(stdio: [FileHandle?], allocatedAttachments: [AllocatedAttachment]) async throws {
80+
/// - Parameter dynamicEnv: Optional start-time environment overrides passed from the API service.
81+
public func bootstrap(stdio: [FileHandle?], allocatedAttachments: [AllocatedAttachment], dynamicEnv: [String: String] = [:]) async throws {
8182
let request = XPCMessage(route: SandboxRoutes.bootstrap.rawValue)
8283

8384
for (i, h) in stdio.enumerated() {
@@ -96,8 +97,9 @@ extension SandboxClient {
9697
}
9798
}
9899

99-
if let sshAuthSocketPath {
100-
request.set(key: SandboxKeys.sshAuthSocketPath.rawValue, value: sshAuthSocketPath)
100+
if !dynamicEnv.isEmpty {
101+
let encodedDynamicEnv = try JSONEncoder().encode(dynamicEnv)
102+
request.set(key: SandboxKeys.dynamicEnv.rawValue, value: encodedDynamicEnv)
101103
}
102104

103105
do {

Sources/Services/ContainerSandboxService/Client/SandboxKeys.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public enum SandboxKeys: String {
4343
/// Container statistics
4444
case statistics
4545

46+
/// SSH agent socket path supplied at bootstrap time (current client shell).
47+
case dynamicEnv
48+
4649
/// Network resource keys.
4750
case allocatedAttachments
4851
case networkAdditionalData

Sources/Services/ContainerSandboxService/Server/SandboxService.swift

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,38 @@ public actor SandboxService {
108108
return URL(fileURLWithPath: resolved.path)
109109
}
110110

111+
/// Merges start-time environment overrides into a base environment list.
112+
/// Existing keys are replaced in-place and new keys are appended.
113+
/// Made static for unit testability.
114+
public static func mergedEnvironmentVariables(base: [String], overrides: [String: String]) -> [String] {
115+
guard !overrides.isEmpty else {
116+
return base
117+
}
118+
119+
var env = base
120+
var pending = overrides
121+
122+
for (index, entry) in env.enumerated() {
123+
guard let separator = entry.firstIndex(of: "=") else {
124+
continue
125+
}
126+
127+
let key = String(entry[..<separator])
128+
if let overrideValue = pending.removeValue(forKey: key) {
129+
env[index] = "\(key)=\(overrideValue)"
130+
}
131+
}
132+
133+
for key in pending.keys.sorted() {
134+
guard let value = pending[key] else {
135+
continue
136+
}
137+
env.append("\(key)=\(value)")
138+
}
139+
140+
return env
141+
}
142+
111143
/// Create an instance with a bundle that describes the container.
112144
///
113145
/// - Parameters:
@@ -177,10 +209,15 @@ public actor SandboxService {
177209
try bundle.createLogFile()
178210

179211
var config = try bundle.configuration
180-
// Extract sshAuthSocketPath from the XPC request; if present and config.ssh is true,
181-
// we mount the socket and set SSH_AUTH_SOCK in the process environment (replacing the
182-
// previous behavior that used the sandbox's launch env).
183-
let bootstrapSshAuthPath = message.string(key: SandboxKeys.sshAuthSocketPath.rawValue)
212+
// Extract dynamic env overrides from the XPC request; when SSH forwarding is enabled,
213+
// SSH_AUTH_SOCK from this map provides the host socket path to mount.
214+
let dynamicEnv: [String: String] =
215+
if let data = message.dataNoCopy(key: SandboxKeys.dynamicEnv.rawValue) {
216+
try JSONDecoder().decode([String: String].self, from: data)
217+
} else {
218+
[:]
219+
}
220+
let bootstrapSshAuthPath = dynamicEnv[Self.sshAuthSocketEnvVar]
184221

185222
if config.ssh {
186223
if let resolved = Self.resolveSSHAuthSocketHostPath(config: config, bootstrapOverridePath: bootstrapSshAuthPath) {
@@ -273,7 +310,12 @@ public actor SandboxService {
273310
let id = config.id
274311
let rootfs = try bundle.containerRootfs.asMount
275312
let container = try LinuxContainer(id, rootfs: rootfs, vmm: vmm, logger: self.log) { czConfig in
276-
try Self.configureContainer(czConfig: &czConfig, config: config, bootstrapOverridePath: bootstrapSshAuthPath)
313+
try Self.configureContainer(
314+
czConfig: &czConfig,
315+
config: config,
316+
bootstrapOverridePath: bootstrapSshAuthPath,
317+
dynamicEnv: dynamicEnv
318+
)
277319
czConfig.interfaces = interfaces
278320
czConfig.process.stdout = stdout
279321
czConfig.process.stderr = stderr
@@ -899,7 +941,8 @@ public actor SandboxService {
899941
private static func configureContainer(
900942
czConfig: inout LinuxContainer.Configuration,
901943
config: ContainerConfiguration,
902-
bootstrapOverridePath: String? = nil
944+
bootstrapOverridePath: String? = nil,
945+
dynamicEnv: [String: String] = [:]
903946
) throws {
904947
czConfig.cpus = config.resources.cpus
905948
czConfig.memoryInBytes = config.resources.memoryInBytes
@@ -958,7 +1001,12 @@ public actor SandboxService {
9581001
searchDomains: dns.searchDomains, options: dns.options)
9591002
}
9601003

961-
try Self.configureInitialProcess(czConfig: &czConfig, config: config, bootstrapOverridePath: bootstrapOverridePath)
1004+
try Self.configureInitialProcess(
1005+
czConfig: &czConfig,
1006+
config: config,
1007+
bootstrapOverridePath: bootstrapOverridePath,
1008+
dynamicEnv: dynamicEnv
1009+
)
9621010
}
9631011

9641012
private func getDefaultNameservers(allocatedAttachments: [AllocatedAttachment]) async throws -> [String] {
@@ -976,17 +1024,20 @@ public actor SandboxService {
9761024
private static func configureInitialProcess(
9771025
czConfig: inout LinuxContainer.Configuration,
9781026
config: ContainerConfiguration,
979-
bootstrapOverridePath: String? = nil
1027+
bootstrapOverridePath: String? = nil,
1028+
dynamicEnv: [String: String] = [:]
9801029
) throws {
9811030
let process = config.initProcess
9821031

9831032
czConfig.process.arguments = [process.executable] + process.arguments
984-
czConfig.process.environmentVariables = process.environment
1033+
czConfig.process.environmentVariables = Self.mergedEnvironmentVariables(
1034+
base: process.environment,
1035+
overrides: dynamicEnv
1036+
)
9851037

9861038
if Self.sshAuthSocketHostUrl(config: config, bootstrapOverridePath: bootstrapOverridePath) != nil {
987-
if !czConfig.process.environmentVariables.contains(where: { $0.starts(with: "\(Self.sshAuthSocketEnvVar)=") }) {
988-
czConfig.process.environmentVariables.append("\(Self.sshAuthSocketEnvVar)=\(Self.sshAuthSocketGuestPath)")
989-
}
1039+
czConfig.process.environmentVariables.removeAll(where: { $0.starts(with: "\(Self.sshAuthSocketEnvVar)=") })
1040+
czConfig.process.environmentVariables.append("\(Self.sshAuthSocketEnvVar)=\(Self.sshAuthSocketGuestPath)")
9901041
}
9911042

9921043
czConfig.process.terminal = process.terminal

0 commit comments

Comments
 (0)