Skip to content

Commit ab40619

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix/container-clean-volume-trim
2 parents 82d2df0 + 520371c commit ab40619

6 files changed

Lines changed: 133 additions & 10 deletions

File tree

Sources/ContainerBuild/Builder.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ public struct Builder: Sendable {
268268
public let contentStore: ContentStore
269269
public let buildArgs: [String]
270270
public let secrets: [String: Data]
271+
public let ssh: String
271272
public let contextDir: String
272273
public let dockerfile: Data
273274
public let dockerignore: Data?
@@ -289,6 +290,7 @@ public struct Builder: Sendable {
289290
contentStore: ContentStore,
290291
buildArgs: [String],
291292
secrets: [String: Data],
293+
ssh: String,
292294
contextDir: String,
293295
dockerfile: Data,
294296
dockerignore: Data?,
@@ -309,6 +311,7 @@ public struct Builder: Sendable {
309311
self.contentStore = contentStore
310312
self.buildArgs = buildArgs
311313
self.secrets = secrets
314+
self.ssh = ssh
312315
self.contextDir = contextDir
313316
self.dockerfile = dockerfile
314317
self.dockerignore = dockerignore
@@ -356,6 +359,9 @@ public struct Builder: Sendable {
356359
for (id, data) in config.secrets {
357360
metadata.addString(id + "=" + data.base64EncodedString(), forKey: "secrets")
358361
}
362+
if config.ssh == "default" {
363+
metadata.addString("default", forKey: "ssh")
364+
}
359365
for output in config.exports {
360366
metadata.addString(try output.stringValue, forKey: "outputs")
361367
}

Sources/ContainerCommands/BuildCommand.swift

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ extension Application {
125125

126126
var secrets: [String: SecretType] = [:]
127127

128+
@Option(
129+
name: .long,
130+
help: ArgumentHelp("Forward SSH agent authentication to the build (format: default)", valueName: "default")
131+
)
132+
var ssh: String = ""
133+
128134
@Option(name: [.short, .customLong("tag")], help: ArgumentHelp("Name for the built image", valueName: "name"))
129135
var targetImageNames: [String] = {
130136
[UUID().uuidString.lowercased()]
@@ -165,12 +171,26 @@ extension Application {
165171
progress.set(description: "Dialing builder")
166172

167173
let dnsNameservers = self.dns.nameservers
168-
let builder: Builder? = try await withThrowingTaskGroup(of: Builder.self) { [vsockPort, cpus, memory, dnsNameservers] group in
174+
175+
// Ensure the builder is started (or restarted) with the correct SSH configuration
176+
// before attempting to dial. This handles the case where the builder is already
177+
// running but was not started with SSH forwarding enabled.
178+
try await BuilderStart.start(
179+
cpus: cpus,
180+
memory: memory,
181+
log: log,
182+
ssh: ssh == "default",
183+
dnsNameservers: dnsNameservers,
184+
progressUpdate: progress.handler,
185+
containerSystemConfig: containerSystemConfig,
186+
)
187+
188+
let builder: Builder? = try await withThrowingTaskGroup(of: Builder.self) { [vsockPort, cpus, memory, dnsNameservers, ssh] group in
169189
defer {
170190
group.cancelAll()
171191
}
172192

173-
group.addTask { [vsockPort, cpus, memory, log, dnsNameservers] in
193+
group.addTask { [vsockPort, cpus, memory, log, dnsNameservers, ssh] in
174194
let client = ContainerClient()
175195
while true {
176196
do {
@@ -192,6 +212,7 @@ extension Application {
192212
cpus: cpus,
193213
memory: memory,
194214
log: log,
215+
ssh: ssh == "default",
195216
dnsNameservers: dnsNameservers,
196217
progressUpdate: progress.handler,
197218
containerSystemConfig: containerSystemConfig,
@@ -349,13 +370,15 @@ extension Application {
349370
}()
350371
group.addTask {
351372
[
352-
terminal, buildArg, secretsData, contextDir, ignoreFileData, label, noCache, target, quiet, cacheIn, cacheOut, pull, exports, imageNames, tempURL, log,
373+
terminal, buildArg, secretsData, ssh, contextDir, ignoreFileData, label, noCache, target, quiet, cacheIn, cacheOut, pull, exports, imageNames, tempURL,
374+
log
353375
] in
354376
let config = Builder.BuildConfig(
355377
buildID: buildID,
356378
contentStore: RemoteContentStoreClient(),
357379
buildArgs: buildArg,
358380
secrets: secretsData,
381+
ssh: ssh,
359382
contextDir: contextDir,
360383
dockerfile: buildFileData,
361384
dockerignore: ignoreFileData,
@@ -510,6 +533,17 @@ extension Application {
510533
throw ValidationError("secret bad value \(parts[1])")
511534
}
512535
}
536+
537+
switch ssh {
538+
case "":
539+
break
540+
case "default" where ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] != nil:
541+
break
542+
case "default":
543+
throw ValidationError("--ssh default requires SSH_AUTH_SOCK to be set")
544+
default:
545+
throw ValidationError("only --ssh default is currently supported")
546+
}
513547
}
514548
}
515549
}

Sources/ContainerCommands/Builder/BuilderStart.swift

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ extension Application {
8484
cpus: Int64?,
8585
memory: String?,
8686
log: Logger,
87+
ssh: Bool = false,
8788
dnsNameservers: [String] = [],
8889
dnsDomain: String? = nil,
8990
dnsSearchDomains: [String] = [],
@@ -150,6 +151,9 @@ extension Application {
150151
let imageChanged = existingImage != builderImage
151152
let cpuChanged = existingResources.cpus != resources.cpus
152153
let memChanged = existingResources.memoryInBytes != resources.memoryInBytes
154+
let sshForwarded = existingContainer.configuration.ssh
155+
let sshWanted = ssh && ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] != nil
156+
let sshChanged = sshForwarded != sshWanted
153157
let dnsChanged = {
154158
if !dnsNameservers.isEmpty {
155159
return existingDNS?.nameservers != dnsNameservers
@@ -168,7 +172,7 @@ extension Application {
168172

169173
switch existingContainer.status {
170174
case .running:
171-
guard imageChanged || cpuChanged || memChanged || envChanged || dnsChanged else {
175+
guard imageChanged || cpuChanged || memChanged || envChanged || dnsChanged || sshChanged else {
172176
// If image, mem, cpu, env, and DNS are the same, continue using the existing builder
173177
return
174178
}
@@ -178,11 +182,22 @@ extension Application {
178182
case .stopped:
179183
// If the builder is stopped and matches our requirements, start it
180184
// Otherwise, delete it and create a new one
181-
guard imageChanged || cpuChanged || memChanged || envChanged || dnsChanged else {
182-
try await startBuildKit(client: client, id: existingContainer.id, progressUpdate, nil)
183-
return
185+
if imageChanged || cpuChanged || memChanged || envChanged || dnsChanged || sshChanged {
186+
try? await client.delete(id: existingContainer.id)
187+
} else {
188+
do {
189+
try await startBuildKit(client: client, id: existingContainer.id, progressUpdate, nil)
190+
return
191+
} catch {
192+
log.warning(
193+
"failed to restart existing stopped BuildKit container, recreating it",
194+
metadata: [
195+
"id": "\(existingContainer.id)",
196+
"error": "\(error)",
197+
])
198+
}
199+
try? await client.delete(id: existingContainer.id)
184200
}
185-
try await client.delete(id: existingContainer.id)
186201
case .stopping:
187202
throw ContainerizationError(
188203
.invalidState,
@@ -242,6 +257,7 @@ extension Application {
242257

243258
var config = ContainerConfiguration(id: Builder.builderContainerId, image: imageDesc, process: processConfig)
244259
config.resources = resources
260+
config.ssh = ssh && ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] != nil
245261
config.labels = [
246262
ResourceLabelKeys.plugin: "builder",
247263
ResourceLabelKeys.role: ResourceRoleValues.builder,

Tests/IntegrationTests/Build/TestCLIBuilderLifecycleSerial.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,69 @@ struct TestCLIBuilderLifecycleSerial {
6767
"NO_COLOR should be forwarded to the buildkit container")
6868
}
6969
}
70+
71+
@Test func testBuildWithSSHDefaultForwarding() async throws {
72+
try await ContainerFixture.with { f in
73+
let socketDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
74+
try FileManager.default.createDirectory(at: socketDir, withIntermediateDirectories: true)
75+
defer {
76+
try? FileManager.default.removeItem(at: socketDir)
77+
}
78+
79+
let socketPath = socketDir.appendingPathComponent("ssh-auth.sock").path
80+
81+
let serverFd = socket(AF_UNIX, SOCK_STREAM, 0)
82+
precondition(serverFd >= 0, "socket() failed")
83+
defer {
84+
Darwin.close(serverFd)
85+
}
86+
87+
var addr = sockaddr_un()
88+
addr.sun_family = sa_family_t(AF_UNIX)
89+
withUnsafeMutableBytes(of: &addr.sun_path) { bytes in
90+
socketPath.withCString { cStr in
91+
bytes.copyMemory(from: UnsafeRawBufferPointer(start: cStr, count: socketPath.utf8.count + 1))
92+
}
93+
}
94+
95+
let bindResult = withUnsafePointer(to: addr) { ptr in
96+
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPtr in
97+
bind(serverFd, sockaddrPtr, socklen_t(MemoryLayout<sockaddr_un>.size))
98+
}
99+
}
100+
precondition(bindResult == 0, "bind() failed: \(errno)")
101+
precondition(listen(serverFd, 5) == 0, "listen() failed")
102+
103+
let acceptThread = Thread {
104+
while true {
105+
let clientFd = accept(serverFd, nil, nil)
106+
if clientFd < 0 { break }
107+
Darwin.close(clientFd)
108+
}
109+
}
110+
acceptThread.start()
111+
112+
let dir = try f.createTempDir()
113+
try f.createContext(
114+
dir: dir,
115+
dockerfile: """
116+
FROM ghcr.io/linuxcontainers/alpine:3.20
117+
RUN --mount=type=ssh \\
118+
test -n "$SSH_AUTH_SOCK" && \\
119+
test -S "$SSH_AUTH_SOCK"
120+
""")
121+
122+
let image = "registry.local/ssh-default-forwarding:\(UUID().uuidString)"
123+
try f.run(
124+
[
125+
"build",
126+
"--ssh", "default",
127+
"-f", dir.appending("Dockerfile").string,
128+
"-t", image,
129+
dir.appending("context").string,
130+
], env: ["SSH_AUTH_SOCK": socketPath]
131+
).check()
132+
try f.assertImageBuilt(image)
133+
}
134+
}
70135
}

Tests/IntegrationTests/Containers/TestCLIExecCommand.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,15 @@ struct TestCLIExecCommand {
7777
try f.doStart(name)
7878
try await f.waitForContainerRunning(name)
7979

80-
let output = try f.doExec(name, cmd: ["sleep", "10"], detach: true)
80+
// Generous duration: ContainersService's host-wide lock can queue this exec's start behind other containers' multi-second VM boots.
81+
let output = try f.doExec(name, cmd: ["sleep", "60"], detach: true)
8182
try #require(
8283
output.trimmingCharacters(in: .whitespacesAndNewlines) == name,
8384
"exec --detach should print the container name")
8485

8586
let ps = try f.doExec(name, cmd: ["ps", "aux"])
8687
.trimmingCharacters(in: .whitespacesAndNewlines)
87-
try #require(ps.contains("sleep 10"), "detached 'sleep 10' should appear in ps output")
88+
try #require(ps.contains("sleep 60"), "detached 'sleep 60' should appear in ps output")
8889

8990
try f.doStop(name)
9091
}

docs/command-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ container build [<options>] [<context-dir>]
157157
* `--pull`: Pull latest image
158158
* `-q, --quiet`: Suppress build output
159159
* `--secret <id=key,...>`: Set build-time secrets (format: id=<key>[,env=<ENV_VAR>|,src=<local/path>])
160+
* `--ssh <default>`: Forward SSH agent authentication to the build. Only `--ssh default` is currently supported.
160161
* `-t, --tag <name>`: Name for the built image (can be specified multiple times)
161162
* `--target <stage>`: Set the target build stage
162163
* `--vsock-port <port>`: Builder shim vsock port (default: 8088)

0 commit comments

Comments
 (0)