Skip to content

Commit 053190c

Browse files
committed
Merge upstream main into container clean command
2 parents 54a0843 + 39f12ca commit 053190c

80 files changed

Lines changed: 3038 additions & 2285 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Package.resolved

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import PackageDescription
2222

2323
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
2424
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
25-
let builderShimVersion = "0.13.0"
26-
let scVersion = "0.38.0"
25+
let builderShimVersion = "0.13.1"
26+
let scVersion = "0.40.1"
2727

2828
let package = Package(
2929
name: "container",
@@ -143,6 +143,7 @@ let package = Package(
143143
.product(name: "Containerization", package: "containerization"),
144144
.product(name: "ContainerizationArchive", package: "containerization"),
145145
.product(name: "ContainerizationOCI", package: "containerization"),
146+
.product(name: "ContainerizationOS", package: "containerization"),
146147
.product(name: "ArgumentParser", package: "swift-argument-parser"),
147148
.product(name: "GRPCCore", package: "grpc-swift-2"),
148149
.product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"),
@@ -574,12 +575,18 @@ let package = Package(
574575
name: "ContainerTestSupport",
575576
dependencies: [
576577
.product(name: "AsyncHTTPClient", package: "async-http-client"),
578+
.product(name: "Containerization", package: "containerization"),
579+
.product(name: "ContainerizationArchive", package: "containerization"),
577580
.product(name: "ContainerizationExtras", package: "containerization"),
578581
.product(name: "Logging", package: "swift-log"),
582+
.product(name: "NIOCore", package: "swift-nio"),
583+
.product(name: "NIOHTTP1", package: "swift-nio"),
584+
.product(name: "NIOPosix", package: "swift-nio"),
579585
.product(name: "SystemPackage", package: "swift-system"),
580586
.product(name: "TOML", package: "swift-toml"),
581587
"ContainerLog",
582588
"ContainerPersistence",
589+
"ContainerPlugin",
583590
"ContainerResource",
584591
]
585592
),

Sources/ContainerBuild/BuildFSSync.swift

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,49 @@ import CryptoKit
2222
import Foundation
2323
import GRPCCore
2424

25+
/// Handles the `fssync` stage of the build protocol.
26+
///
27+
/// When BuildKit needs build-context files it sends `Walk`, `Read`, and `Info`
28+
/// requests to the shim, which proxies them over the gRPC stream to this actor.
29+
///
30+
/// ## Primary path: Walk (tar mode)
31+
///
32+
/// `Walk` is the primary data path. The host packs all requested context paths
33+
/// into a tar archive and streams it to the shim. The shim unpacks the tar to a
34+
/// local cache and presents the files to BuildKit via `DiffCopy`. BuildKit then
35+
/// issues `PACKET_REQ` for regular files it needs; the shim serves those from
36+
/// the local cache without any further calls to the host.
37+
///
38+
/// When a context path is a symlink whose target lies within the context root,
39+
/// ``walk(_:_:_:)`` adds the target to the archive alongside the symlink so
40+
/// BuildKit can dereference it during `COPY`/`ADD` processing.
41+
///
42+
/// ## Fallback path: Info + Read
43+
///
44+
/// `FS.Open()` in the shim falls back to `Info` followed by `Read` calls when
45+
/// its local checksum cache is unpopulated (a narrow race window at the start of
46+
/// a build). These paths are not exercised during a normal build.
47+
///
48+
/// ## Symlink safety
49+
///
50+
/// The host enforces that no file served to the builder resolves to a path
51+
/// outside the context root. If any component of a requested path is a symlink
52+
/// whose target lies outside the context root the request is rejected.
53+
/// Dockerignore filtering is **not** applied here; the shim applies it after
54+
/// unpacking the tar.
2555
actor BuildFSSync: BuildPipelineHandler {
2656
let contextDir: URL
2757

2858
init(_ contextDir: URL) throws {
59+
let resolved = contextDir.resolvingSymlinksInPath()
2960
guard FileManager.default.fileExists(atPath: contextDir.cleanPath) else {
3061
throw Error.contextNotFound(contextDir.cleanPath)
3162
}
32-
guard try contextDir.isDir() else {
63+
guard resolved.isDirectory else {
3364
throw Error.contextIsNotDirectory(contextDir.cleanPath)
3465
}
3566

36-
self.contextDir = contextDir
67+
self.contextDir = resolved
3768
}
3869

3970
nonisolated func accept(_ packet: ServerStream) throws -> Bool {
@@ -63,6 +94,11 @@ actor BuildFSSync: BuildPipelineHandler {
6394
}
6495
}
6596

97+
/// Serves the content of a single context file to the shim.
98+
///
99+
/// Called only via the shim's `FS.Open()` fallback path, not during a
100+
/// normal `Walk`-based build. Rejects any path whose symlink chain resolves
101+
/// outside the context root.
66102
func read(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: BuildTransfer, _ buildID: String) async throws {
67103
let offset: UInt64 = packet.offset() ?? 0
68104
let size: Int = packet.len() ?? 0
@@ -79,6 +115,10 @@ actor BuildFSSync: BuildPipelineHandler {
79115
path = URL(filePath: self.contextDir.cleanPath)
80116
path.append(components: packet.source.cleanPathComponent)
81117
}
118+
let resolved = path.resolvingSymlinksInPath()
119+
guard self.contextDir.parentOf(resolved) else {
120+
throw Error.pathIsNotChild(resolved.cleanPath, self.contextDir.cleanPath)
121+
}
82122
let data = try {
83123
if try path.isDir() {
84124
return Data()
@@ -95,6 +135,12 @@ actor BuildFSSync: BuildPipelineHandler {
95135
sender.yield(response)
96136
}
97137

138+
/// Returns metadata (mode, size, modification time, uid/gid) for a single
139+
/// context path.
140+
///
141+
/// Called only via the shim's `FS.Open()` fallback path, not during a
142+
/// normal `Walk`-based build. Must reject paths that escape the context root
143+
/// via symlinks for the same reasons as ``read(_:_:_:)``.
98144
func info(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: BuildTransfer, _ buildID: String) async throws {
99145
let path: URL
100146
if packet.source.hasPrefix("/") {
@@ -105,6 +151,10 @@ actor BuildFSSync: BuildPipelineHandler {
105151
.appendingPathComponent(packet.source)
106152
.standardizedFileURL
107153
}
154+
let resolved = path.resolvingSymlinksInPath()
155+
guard self.contextDir.parentOf(resolved) else {
156+
throw Error.pathIsNotChild(resolved.cleanPath, self.contextDir.cleanPath)
157+
}
108158
let transfer = try path.buildTransfer(id: packet.id, contextDir: self.contextDir, complete: true)
109159
var response = ClientStream()
110160
response.buildID = buildID
@@ -127,6 +177,23 @@ actor BuildFSSync: BuildPipelineHandler {
127177
}
128178
}
129179

180+
/// Packs requested context paths into a tar archive and streams it to the shim.
181+
///
182+
/// This is the primary data path for build-context transfer. BuildKit sends
183+
/// a `Walk` request whose `followpaths` field names the context paths needed
184+
/// for the current build step (e.g. the source of a `COPY` instruction).
185+
/// The host resolves those globs, builds an entry set, and passes it to
186+
/// `Archiver.compress` to produce the tar.
187+
///
188+
/// For any symlink in the entry set whose target lies within the context
189+
/// root, the target is added to the entry set so BuildKit can dereference
190+
/// the symlink during `COPY`/`ADD` processing without a separate request.
191+
/// Symlinks whose targets lie outside the context root are included as
192+
/// symlink entries but their targets are not; BuildKit will resolve them
193+
/// against the shim's local filesystem on Linux, not the macOS host.
194+
///
195+
/// Dockerignore filtering is the shim's responsibility and is applied after
196+
/// the tar is unpacked; this method has no knowledge of `.dockerignore`.
130197
func walk(
131198
_ sender: AsyncStream<ClientStream>.Continuation,
132199
_ packet: BuildTransfer,
@@ -334,16 +401,10 @@ actor BuildFSSync: BuildPipelineHandler {
334401
let target: String
335402

336403
init(path: URL, contextDir: URL) throws {
337-
if path.isSymlink {
338-
let target: URL = path.resolvingSymlinksInPath()
339-
if contextDir.parentOf(target) {
340-
self.target = target.relativePathFrom(from: path)
341-
} else {
342-
self.target = target.cleanPath
343-
}
344-
} else {
345-
self.target = ""
346-
}
404+
// Always report the literal, unresolved on-disk symlink target —
405+
// the same value tar mode provides via Archiver's use of
406+
// destinationOfSymbolicLink — rather than a host-resolved path.
407+
self.target = path.isSymlink ? try FileManager.default.destinationOfSymbolicLink(atPath: path.cleanPath) : ""
347408

348409
self.name = try path.relativeChildPath(to: contextDir)
349410
self.modTime = try path.modTime()

Sources/ContainerBuild/BuildImageResolver.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ import GRPCCore
2323
import Logging
2424
import TerminalProgress
2525

26+
/// Handles the `resolver` stage of the build protocol.
27+
///
28+
/// Resolves image references on behalf of BuildKit: authenticates with
29+
/// registries, pulls missing base-image manifests and layers, and stores
30+
/// them in the local content store. BuildKit delegates these operations to
31+
/// the host because registry credentials and network access live on the
32+
/// macOS side, not inside the builder VM.
2633
struct BuildImageResolver: BuildPipelineHandler {
2734
let contentStore: ContentStore
2835
let quiet: Bool

Sources/ContainerBuild/BuildPipelineHandler.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,61 @@ import Foundation
1818
import GRPCCore
1919
import NIO
2020

21+
/// A handler for one stage of the build protocol.
22+
///
23+
/// The build pipeline multiplexes a single bidirectional gRPC stream between
24+
/// the macOS host and the builder shim. Each packet carries a stage tag;
25+
/// a handler claims packets for its stage via ``accept(_:)`` and processes
26+
/// them via ``handle(_:_:)``.
2127
protocol BuildPipelineHandler: Sendable {
2228
func accept(_ packet: ServerStream) throws -> Bool
2329
func handle(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ServerStream) async throws
2430
}
2531

32+
/// Drives a build session by routing packets from the builder shim to the
33+
/// appropriate handler.
34+
///
35+
/// ## Three-tier architecture
36+
///
37+
/// Builds involve three components with distinct responsibilities:
38+
///
39+
/// **macOS host (`BuildPipeline` / its handlers)**
40+
/// Serves resources to the builder shim over a bidirectional gRPC stream.
41+
/// Responsibilities include:
42+
/// - Packing requested build-context files into a tar archive (``BuildFSSync``).
43+
/// - Proxying image-layer blobs from the local content store (``BuildRemoteContentProxy``).
44+
/// - Resolving and pulling base images (``BuildImageResolver``).
45+
/// - Relaying builder stdout/stderr to the terminal (``BuildStdio``).
46+
/// - Enforcing the context root boundary: directory traversal uses `openat(O_NOFOLLOW)`
47+
/// at every descent step, and every individual file request resolves symlinks to their
48+
/// canonical path before verifying containment within the context root.
49+
///
50+
/// **Builder shim (`container-builder-shim`)**
51+
/// A Go process running inside a Linux VM that bridges the host gRPC stream
52+
/// and BuildKit's `filesync` gRPC interface. Responsibilities include:
53+
/// - Receiving the context tar from the host, unpacking it to a local cache,
54+
/// and presenting the result to BuildKit via `DiffCopy`.
55+
/// - Applying dockerignore exclusions (received from BuildKit as
56+
/// `exclude-patterns` metadata) when walking the unpacked cache.
57+
/// - Passing `followpaths` from BuildKit to the host so the host knows which
58+
/// context paths to include in the tar.
59+
///
60+
/// **BuildKit**
61+
/// Parses and executes the Dockerfile. Responsibilities include:
62+
/// - Sending `Walk` requests with `followpaths` derived from each `COPY`/`ADD`
63+
/// source and `exclude-patterns` derived from `.dockerignore`.
64+
/// - Dereferencing symlinks, recursing into directories, and applying all
65+
/// other COPY/ADD transfer semantics on the unpacked context the shim provides.
66+
///
67+
/// ## Packet flow
68+
///
69+
/// ```
70+
/// BuildKit ──► shim DiffCopy ──► host Walk (tar of context files)
71+
/// ◄── tar archive
72+
/// ◄── PACKET_STAT per file (after shim unpacks + filters)
73+
/// ──► PACKET_REQ for each regular file
74+
/// ◄── PACKET_DATA (shim reads from local unpacked cache)
75+
/// ```
2676
public actor BuildPipeline {
2777
let handlers: [BuildPipelineHandler]
2878
public init(_ config: Builder.BuildConfig) async throws {

Sources/ContainerBuild/BuildRemoteContentProxy.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ import ContainerizationOCI
2121
import Foundation
2222
import GRPCCore
2323

24+
/// Handles the `content-store` stage of the build protocol.
25+
///
26+
/// Proxies image-layer blob requests from BuildKit to the host's local
27+
/// containerd content store. BuildKit issues these requests when it needs
28+
/// base-image layers that are not already present in the builder VM.
2429
struct BuildRemoteContentProxy: BuildPipelineHandler {
2530
let local: ContentStore
2631

Sources/ContainerBuild/BuildStdio.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import Foundation
1919
import GRPCCore
2020
import NIO
2121

22+
/// Handles the stdio stage of the build protocol.
23+
///
24+
/// Relays builder stdout/stderr from the shim to the client terminal.
25+
/// Build output (layer download progress, `RUN` command output, etc.) flows
26+
/// through this handler and is written directly to the configured file handle.
2227
actor BuildStdio: BuildPipelineHandler {
2328
public let quiet: Bool
2429
public let handle: FileHandle

0 commit comments

Comments
 (0)