Initial commit

This commit is contained in:
Kathryn Baldauf
2025-06-03 15:23:07 -07:00
commit d5f30b8e3e
225 changed files with 27705 additions and 0 deletions
@@ -0,0 +1,154 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Containerization
import ContainerizationOCI
public typealias IO = Com_Apple_Container_Build_V1_IO
public typealias InfoRequest = Com_Apple_Container_Build_V1_InfoRequest
public typealias InfoResponse = Com_Apple_Container_Build_V1_InfoResponse
public typealias ClientStream = Com_Apple_Container_Build_V1_ClientStream
public typealias ServerStream = Com_Apple_Container_Build_V1_ServerStream
public typealias ImageTransfer = Com_Apple_Container_Build_V1_ImageTransfer
public typealias BuildTransfer = Com_Apple_Container_Build_V1_BuildTransfer
public typealias BuilderClient = Com_Apple_Container_Build_V1_BuilderNIOClient
public typealias BuilderClientAsync = Com_Apple_Container_Build_V1_BuilderAsyncClient
public typealias BuilderClientProtocol = Com_Apple_Container_Build_V1_BuilderClientProtocol
public typealias BuilderClientAsyncProtocol = Com_Apple_Container_Build_V1_BuilderAsyncClient
extension BuildTransfer {
func stage() -> String? {
let stage = self.metadata["stage"]
return stage == "" ? nil : stage
}
func method() -> String? {
let method = self.metadata["method"]
return method == "" ? nil : method
}
func includePatterns() -> [String]? {
guard let includePatternsString = self.metadata["include-patterns"] else {
return nil
}
return includePatternsString == "" ? nil : includePatternsString.components(separatedBy: ",")
}
func followPaths() -> [String]? {
guard let followPathString = self.metadata["followpaths"] else {
return nil
}
return followPathString == "" ? nil : followPathString.components(separatedBy: ",")
}
func mode() -> String? {
self.metadata["mode"]
}
func size() -> Int? {
guard let sizeStr = self.metadata["size"] else {
return nil
}
return sizeStr == "" ? nil : Int(sizeStr)
}
func offset() -> UInt64? {
guard let offsetStr = self.metadata["offset"] else {
return nil
}
return offsetStr == "" ? nil : UInt64(offsetStr)
}
func len() -> Int? {
guard let lenStr = self.metadata["length"] else {
return nil
}
return lenStr == "" ? nil : Int(lenStr)
}
}
extension ImageTransfer {
func stage() -> String? {
self.metadata["stage"]
}
func method() -> String? {
self.metadata["method"]
}
func ref() -> String? {
self.metadata["ref"]
}
func platform() throws -> Platform? {
let metadata = self.metadata
guard let platform = metadata["platform"] else {
return nil
}
return try Platform(from: platform)
}
func mode() -> String? {
self.metadata["mode"]
}
func size() -> Int? {
let metadata = self.metadata
guard let sizeStr = metadata["size"] else {
return nil
}
return Int(sizeStr)
}
func len() -> Int? {
let metadata = self.metadata
guard let lenStr = metadata["length"] else {
return nil
}
return Int(lenStr)
}
func offset() -> UInt64? {
let metadata = self.metadata
guard let offsetStr = metadata["offset"] else {
return nil
}
return UInt64(offsetStr)
}
}
extension ServerStream {
func getImageTransfer() -> ImageTransfer? {
if case .imageTransfer(let v) = self.packetType {
return v
}
return nil
}
func getBuildTransfer() -> BuildTransfer? {
if case .buildTransfer(let v) = self.packetType {
return v
}
return nil
}
func getIO() -> IO? {
if case .io(let v) = self.packetType {
return v
}
return nil
}
}
+129
View File
@@ -0,0 +1,129 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationArchive
import Foundation
import GRPC
import NIO
actor BuildExporter: BuildPipelineHandler {
let output: OutputStream
let channel: AsyncThrowingStream<(AsyncStream<ClientStream>.Continuation, ServerStream), Swift.Error>.Continuation
public init(output: URL) throws {
guard let output = OutputStream(toFileAtPath: output.absolutePath(), append: true) else {
throw Error.couldNotInitializeOutput(output.absolutePath())
}
self.output = output
self.output.open()
var c: AsyncThrowingStream<(AsyncStream<ClientStream>.Continuation, ServerStream), Swift.Error>.Continuation?
let writeStream: AsyncThrowingStream<(AsyncStream<ClientStream>.Continuation, ServerStream), Swift.Error> = AsyncThrowingStream { continuation in
c = continuation
}
guard let c else {
throw Builder.Error.invalidContinuation
}
self.channel = c
Task.detached {
for try await packet in writeStream {
try await self.write(packet.0, packet.1)
}
}
}
nonisolated func accept(_ packet: ServerStream) throws -> Bool {
guard let buildTransfer = packet.getBuildTransfer() else {
return false
}
guard buildTransfer.stage() == "exporter" else {
return false
}
return true
}
func handle(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ServerStream) async throws {
self.channel.yield((sender, packet)) // guarantees ordering while being non-blocking
}
func write(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ServerStream) async throws {
guard let buildTransfer = packet.getBuildTransfer() else {
throw Error.buildTransferMissing
}
guard buildTransfer.stage() == "exporter" else {
throw Error.invalidStage(buildTransfer.stage() ?? "")
}
let buildID = packet.buildID
if buildTransfer.complete {
var transfer = BuildTransfer()
transfer.id = buildTransfer.id
transfer.direction = .outof
transfer.metadata = [
"os": "linux",
"stage": "exporter",
]
var response = ClientStream()
response.buildID = buildID
response.buildTransfer = transfer
response.packetType = .buildTransfer(transfer)
sender.yield(response)
self.output.close()
return
}
try buildTransfer.data.withUnsafeBytes { rawBuf in
let bufPointer = rawBuf.bindMemory(to: UInt8.self)
if let baseAddr = bufPointer.baseAddress, bufPointer.count > 0 {
let n = self.output.write(baseAddr, maxLength: bufPointer.count)
if n < 0 || n < bufPointer.count {
throw Error.writeError
}
}
}
var transfer = BuildTransfer()
transfer.id = buildTransfer.id
transfer.direction = .outof
transfer.metadata = [
"os": "linux",
"stage": "exporter",
]
var response = ClientStream()
response.buildID = buildID
response.buildTransfer = transfer
response.packetType = .buildTransfer(transfer)
sender.yield(response)
}
}
extension BuildExporter {
enum Error: Swift.Error, CustomStringConvertible {
case buildTransferMissing
case invalidStage(String)
case couldNotInitializeOutput(String)
case writeError
var description: String {
switch self {
case .buildTransferMissing:
return "buildTransfer field missing in packet"
case .invalidStage(let stage):
return "stage \(stage) is invalid, expected 'exporter'"
case .couldNotInitializeOutput(let output):
return "could not open \(output) for writing"
case .writeError:
return "write failed"
}
}
}
}
+498
View File
@@ -0,0 +1,498 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Collections
import ContainerClient
import ContainerizationArchive
import ContainerizationOCI
import Foundation
import GRPC
actor BuildFSSync: BuildPipelineHandler {
let contextDir: URL
init(_ contextDir: URL) throws {
guard FileManager.default.fileExists(atPath: contextDir.cleanPath) else {
throw Error.contextNotFound(contextDir.cleanPath)
}
guard try contextDir.isDir() else {
throw Error.contextIsNotDirectory(contextDir.cleanPath)
}
self.contextDir = contextDir
}
nonisolated func accept(_ packet: ServerStream) throws -> Bool {
guard let buildTransfer = packet.getBuildTransfer() else {
return false
}
guard buildTransfer.stage() == "fssync" else {
return false
}
return true
}
func handle(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ServerStream) async throws {
guard let buildTransfer = packet.getBuildTransfer() else {
throw Error.buildTransferMissing
}
guard let method = buildTransfer.method() else {
throw Error.methodMissing
}
switch try FSSyncMethod(method) {
case .read:
try await self.read(sender, buildTransfer, packet.buildID)
case .info:
try await self.info(sender, buildTransfer, packet.buildID)
case .walk:
try await self.walk(sender, buildTransfer, packet.buildID)
}
}
func read(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: BuildTransfer, _ buildID: String) async throws {
let offset: UInt64 = packet.offset() ?? 0
let size: Int = packet.len() ?? 0
var path: URL = URL(filePath: packet.source.cleanPathComponent)
if !FileManager.default.fileExists(atPath: path.cleanPath) {
path = URL(filePath: self.contextDir.cleanPath)
path.append(components: packet.source.cleanPathComponent)
}
let data = try {
if try path.isDir() {
return Data()
}
let file = try LocalContent(path: path.standardizedFileURL)
return try file.data(offset: offset, length: size) ?? Data()
}()
let transfer = try path.buildTransfer(id: packet.id, contextDir: self.contextDir, complete: true, data: data)
var response = ClientStream()
response.buildID = buildID
response.buildTransfer = transfer
response.packetType = .buildTransfer(transfer)
sender.yield(response)
}
func info(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: BuildTransfer, _ buildID: String) async throws {
var path = self.contextDir
path.append(components: packet.source.cleanPathComponent)
let transfer = try path.buildTransfer(id: packet.id, contextDir: self.contextDir, complete: true)
var response = ClientStream()
response.buildID = buildID
response.buildTransfer = transfer
response.packetType = .buildTransfer(transfer)
sender.yield(response)
}
private struct DirEntry: Hashable {
let url: URL
let isDirectory: Bool
let relativePath: String
func hash(into hasher: inout Hasher) {
hasher.combine(relativePath)
}
static func == (lhs: DirEntry, rhs: DirEntry) -> Bool {
lhs.relativePath == rhs.relativePath
}
}
func walk(
_ sender: AsyncStream<ClientStream>.Continuation,
_ packet: BuildTransfer,
_ buildID: String
) async throws {
let wantsTar = packet.mode() == "tar"
var entries: [String: Set<DirEntry>] = [:]
let followPaths: [String] = packet.followPaths() ?? []
let followPathsWalked = try walk(root: self.contextDir, includePatterns: followPaths)
for url in followPathsWalked {
guard self.contextDir.parentOf(url) else {
continue
}
let relPath = try url.relativeChildPath(to: contextDir)
let parentPath = try url.deletingLastPathComponent().relativeChildPath(to: contextDir)
let entry = DirEntry(url: url, isDirectory: url.hasDirectoryPath, relativePath: relPath)
entries[parentPath, default: []].insert(entry)
if url.isSymlink {
let target: URL = url.resolvingSymlinksInPath()
if self.contextDir.parentOf(target) {
let relPath = try target.relativeChildPath(to: self.contextDir)
let entry = DirEntry(url: target, isDirectory: target.hasDirectoryPath, relativePath: relPath)
let parentPath: String = try target.deletingLastPathComponent().relativeChildPath(to: self.contextDir)
entries[parentPath, default: []].insert(entry)
}
}
}
var fileOrder = [String]()
try processDirectory("", inputEntries: entries, processedPaths: &fileOrder)
if !wantsTar {
let fileInfos = try fileOrder.map { rel -> FileInfo in
try FileInfo(path: contextDir.appendingPathComponent(rel), contextDir: contextDir)
}
let data = try JSONEncoder().encode(fileInfos)
let transfer = BuildTransfer(
id: packet.id,
source: packet.source,
complete: true,
isDir: false,
metadata: [
"os": "linux",
"stage": "fssync",
"mode": "json",
],
data: data
)
var resp = ClientStream()
resp.buildID = buildID
resp.buildTransfer = transfer
resp.packetType = .buildTransfer(transfer)
sender.yield(resp)
return
}
let tarURL = URL.temporaryDirectory
.appendingPathComponent(UUID().uuidString + ".tar")
defer { try? FileManager.default.removeItem(at: tarURL) }
let writerCfg = ArchiveWriterConfiguration(
format: .paxRestricted,
filter: .none)
try Archiver.compress(
source: contextDir,
destination: tarURL,
writerConfiguration: writerCfg
) { url in
guard let rel = try? url.relativeChildPath(to: contextDir) else {
return nil
}
guard let parent = try? url.deletingLastPathComponent().relativeChildPath(to: self.contextDir) else {
return nil
}
guard let items = entries[parent] else {
return nil
}
let include = items.contains { item in
item.relativePath == rel
}
guard include else {
return nil
}
return Archiver.ArchiveEntryInfo(
pathOnHost: url,
pathInArchive: URL(fileURLWithPath: rel))
}
for await chunk in try tarURL.zeroCopyReader() {
let part = BuildTransfer(
id: packet.id,
source: tarURL.path,
complete: false,
isDir: false,
metadata: [
"os": "linux",
"stage": "fssync",
"mode": "tar",
],
data: chunk
)
var resp = ClientStream()
resp.buildID = buildID
resp.buildTransfer = part
resp.packetType = .buildTransfer(part)
sender.yield(resp)
}
let done = BuildTransfer(
id: packet.id,
source: tarURL.path,
complete: true,
isDir: false,
metadata: [
"os": "linux",
"stage": "fssync",
"mode": "tar",
],
data: Data()
)
var finalResp = ClientStream()
finalResp.buildID = buildID
finalResp.buildTransfer = done
finalResp.packetType = .buildTransfer(done)
sender.yield(finalResp)
}
func walk(root: URL, includePatterns: [String]) throws -> [URL] {
let globber = Globber(root)
for p in includePatterns {
try globber.match(p)
}
return Array(globber.results)
}
private func processDirectory(
_ currentDir: String,
inputEntries: [String: Set<DirEntry>],
processedPaths: inout [String]
) throws {
guard let entries = inputEntries[currentDir] else {
return
}
// Sort purely by lexicographical order of relativePath
let sortedEntries = entries.sorted { $0.relativePath < $1.relativePath }
for entry in sortedEntries {
processedPaths.append(entry.relativePath)
if entry.isDirectory {
try processDirectory(
entry.relativePath,
inputEntries: inputEntries,
processedPaths: &processedPaths
)
}
}
}
struct FileInfo: Codable {
let name: String
let modTime: String
let mode: UInt32
let size: UInt64
let isDir: Bool
let uid: UInt32
let gid: UInt32
let target: String
init(path: URL, contextDir: URL) throws {
if path.isSymlink {
let target: URL = path.resolvingSymlinksInPath()
if contextDir.parentOf(target) {
self.target = target.relativePathFrom(from: path)
} else {
self.target = target.cleanPath
}
} else {
self.target = ""
}
self.name = try path.relativeChildPath(to: contextDir)
self.modTime = try path.modTime()
self.mode = try path.mode()
self.size = try path.size()
self.isDir = path.hasDirectoryPath
self.uid = 0
self.gid = 0
}
}
enum FSSyncMethod: String {
case read = "Read"
case info = "Info"
case walk = "Walk"
init(_ method: String) throws {
switch method {
case "Read":
self = .read
case "Info":
self = .info
case "Walk":
self = .walk
default:
throw Error.unknownMethod(method)
}
}
}
}
extension BuildFSSync {
enum Error: Swift.Error, CustomStringConvertible, Equatable {
case buildTransferMissing
case methodMissing
case unknownMethod(String)
case contextNotFound(String)
case contextIsNotDirectory(String)
case couldNotDetermineFileSize(String)
case couldNotDetermineModTime(String)
case couldNotDetermineFileMode(String)
case invalidOffsetSizeForFile(String, UInt64, Int)
case couldNotDetermineUID(String)
case couldNotDetermineGID(String)
case pathIsNotChild(String, String)
var description: String {
switch self {
case .buildTransferMissing:
return "buildTransfer field missing in packet"
case .methodMissing:
return "method is missing in request"
case .unknownMethod(let m):
return "unknown content-store method \(m)"
case .contextNotFound(let path):
return "context dir \(path) not found"
case .contextIsNotDirectory(let path):
return "context \(path) not a directory"
case .couldNotDetermineFileSize(let path):
return "could not determine size of file \(path)"
case .couldNotDetermineModTime(let path):
return "could not determine last modified time of \(path)"
case .couldNotDetermineFileMode(let path):
return "could not determine posix permissions (FileMode) of \(path)"
case .invalidOffsetSizeForFile(let digest, let offset, let size):
return "invalid request for file: \(digest) with offset: \(offset) size: \(size)"
case .couldNotDetermineUID(let path):
return "could not determine UID of file at path: \(path)"
case .couldNotDetermineGID(let path):
return "could not determine GID of file at path: \(path)"
case .pathIsNotChild(let path, let parent):
return "\(path) is not a child of \(parent)"
}
}
}
}
extension BuildTransfer {
fileprivate init(id: String, source: String, complete: Bool, isDir: Bool, metadata: [String: String], data: Data? = nil) {
self.init()
self.id = id
self.source = source
self.direction = .outof
self.complete = complete
self.metadata = metadata
self.isDirectory = isDir
if let data {
self.data = data
}
}
}
extension URL {
fileprivate func size() throws -> UInt64 {
let attrs = try FileManager.default.attributesOfItem(atPath: self.cleanPath)
if let size = attrs[FileAttributeKey.size] as? UInt64 {
return size
}
throw BuildFSSync.Error.couldNotDetermineFileSize(self.cleanPath)
}
fileprivate func modTime() throws -> String {
let attrs = try FileManager.default.attributesOfItem(atPath: self.cleanPath)
if let date = attrs[FileAttributeKey.modificationDate] as? Date {
return date.rfc3339()
}
throw BuildFSSync.Error.couldNotDetermineModTime(self.cleanPath)
}
fileprivate func isDir() throws -> Bool {
let attrs = try FileManager.default.attributesOfItem(atPath: self.cleanPath)
guard let t = attrs[.type] as? FileAttributeType, t == .typeDirectory else {
return false
}
return true
}
fileprivate func mode() throws -> UInt32 {
let attrs = try FileManager.default.attributesOfItem(atPath: self.cleanPath)
if let mode = attrs[FileAttributeKey.posixPermissions] as? NSNumber {
return mode.uint32Value
}
throw BuildFSSync.Error.couldNotDetermineFileMode(self.cleanPath)
}
fileprivate func uid() throws -> UInt32 {
let attrs = try FileManager.default.attributesOfItem(atPath: self.cleanPath)
if let uid = attrs[.ownerAccountID] as? UInt32 {
return uid
}
throw BuildFSSync.Error.couldNotDetermineUID(self.cleanPath)
}
fileprivate func gid() throws -> UInt32 {
let attrs = try FileManager.default.attributesOfItem(atPath: self.cleanPath)
if let gid = attrs[.groupOwnerAccountID] as? UInt32 {
return gid
}
throw BuildFSSync.Error.couldNotDetermineGID(self.cleanPath)
}
fileprivate func buildTransfer(
id: String,
contextDir: URL? = nil,
complete: Bool = false,
data: Data = Data()
) throws -> BuildTransfer {
let p = try {
if let contextDir { return try self.relativeChildPath(to: contextDir) }
return self.cleanPath
}()
return BuildTransfer(
id: id,
source: String(p),
complete: complete,
isDir: try self.isDir(),
metadata: [
"os": "linux",
"stage": "fssync",
"mode": String(try self.mode()),
"size": String(try self.size()),
"modified_at": try self.modTime(),
"uid": String(try self.uid()),
"gid": String(try self.gid()),
],
data: data
)
}
}
extension Date {
fileprivate func rfc3339() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) // Adjust if necessary
return dateFormatter.string(from: self)
}
}
extension String {
var cleanPathComponent: String {
let trimmed = self.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
if let clean = trimmed.removingPercentEncoding {
return clean
}
return trimmed
}
}
@@ -0,0 +1,152 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerClient
import Containerization
import ContainerizationOCI
import Foundation
import GRPC
import Logging
struct BuildImageResolver: BuildPipelineHandler {
let contentStore: ContentStore
public init(_ contentStore: ContentStore) throws {
self.contentStore = contentStore
}
func accept(_ packet: ServerStream) throws -> Bool {
guard let imageTransfer = packet.getImageTransfer() else {
return false
}
guard imageTransfer.stage() == "resolver" else {
return false
}
guard imageTransfer.method() == "/resolve" else {
return false
}
return true
}
func handle(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ServerStream) async throws {
guard let imageTransfer = packet.getImageTransfer() else {
throw Error.imageTransferMissing
}
guard let ref = imageTransfer.ref() else {
throw Error.tagMissing
}
guard let platform = try imageTransfer.platform() else {
throw Error.platformMissing
}
let img = try await {
guard let img = try? await ClientImage.pull(reference: ref, platform: platform) else {
return try await ClientImage.fetch(reference: ref, platform: platform)
}
return img
}()
let index: Index = try await img.index()
let buildID = packet.buildID
let platforms = index.manifests.compactMap { $0.platform }
for pl in platforms {
if pl == platform {
let manifest = try await img.manifest(for: pl)
guard let ociImage: ContainerizationOCI.Image = try await self.contentStore.get(digest: manifest.config.digest) else {
continue
}
let enc = JSONEncoder()
let data = try enc.encode(ociImage)
let transfer = try ImageTransfer(
id: imageTransfer.id,
digest: img.descriptor.digest,
ref: ref,
platform: platform.description,
data: data
)
var response = ClientStream()
response.buildID = buildID
response.imageTransfer = transfer
response.packetType = .imageTransfer(transfer)
sender.yield(response)
return
}
}
throw Error.unknownPlatformForImage(platform.description, ref)
}
}
extension ImageTransfer {
fileprivate init(id: String, digest: String, ref: String, platform: String, data: Data) throws {
self.init()
self.id = id
self.tag = digest
self.metadata = [
"os": "linux",
"stage": "resolver",
"method": "/resolve",
"ref": ref,
"platform": platform,
]
self.complete = true
self.direction = .into
self.data = data
}
}
extension BuildImageResolver {
enum Error: Swift.Error, CustomStringConvertible {
case imageTransferMissing
case tagMissing
case platformMissing
case imageNameMissing
case imageTagMissing
case imageNotFound
case indexDigestMissing(String)
case unknownRegistry(String)
case digestIsNotIndex(String)
case digestIsNotManifest(String)
case unknownPlatformForImage(String, String)
var description: String {
switch self {
case .imageTransferMissing:
return "imageTransfer is missing"
case .tagMissing:
return "tag parameter missing in metadata"
case .platformMissing:
return "platform parameter missing in metadata"
case .imageNameMissing:
return "image name missing in $ref parameter"
case .imageTagMissing:
return "image tag missing in $ref parameter"
case .imageNotFound:
return "image not found"
case .indexDigestMissing(let ref):
return "index digest is missing for image: \(ref)"
case .unknownRegistry(let registry):
return "registry \(registry) is unknown"
case .digestIsNotIndex(let digest):
return "digest \(digest) is not a descriptor to an index"
case .digestIsNotManifest(let digest):
return "digest \(digest) is not a descriptor to a manifest"
case .unknownPlatformForImage(let platform, let ref):
return "platform \(platform) for image \(ref) not found"
}
}
}
}
@@ -0,0 +1,198 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import GRPC
import NIO
protocol BuildPipelineHandler: Sendable {
func accept(_ packet: ServerStream) throws -> Bool
func handle(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ServerStream) async throws
}
public actor BuildPipeline {
let handlers: [BuildPipelineHandler]
public init(_ config: Builder.BuildConfig) async throws {
let exporters: [BuildPipelineHandler] = try config.exports.map { export in
guard let destination = export.destination else {
throw Builder.Error.invalidExport(export.rawValue, "dest is required")
}
return try BuildExporter(output: destination)
}
self.handlers =
exporters + [
try BuildFSSync(URL(filePath: config.contextDir)),
try BuildRemoteContentProxy(config.contentStore),
try BuildImageResolver(config.contentStore),
try BuildStdio(quiet: config.quiet, output: config.terminal?.handle ?? FileHandle.standardError),
]
}
public func run(
sender: AsyncStream<ClientStream>.Continuation,
receiver: GRPCAsyncResponseStream<ServerStream>
) async throws {
defer { sender.finish() }
try await untilFirstError { group in
for try await packet in receiver {
try Task.checkCancellation()
for handler in self.handlers {
try Task.checkCancellation()
guard try handler.accept(packet) else {
continue
}
try Task.checkCancellation()
try await handler.handle(sender, packet)
break
}
}
}
}
/// untilFirstError() throws when any one of its submitted tasks fail.
/// This is useful for asynchronous packet processing scenarios which
/// have the following 3 requirements:
/// - the packet should be processed without blocking I/O
/// - the packet stream is never-ending
/// - when the first task fails, the error needs to be propagated to the caller
///
/// Usage:
///
/// ```
/// try await untilFirstError { group in
/// for try await packet in receiver {
/// group.addTask {
/// try await handler.handle(sender, packet)
/// }
/// }
/// }
/// ```
///
///
/// WithThrowingTaskGroup cannot accomplish this because it
/// doesn't provide a mechanism to exit when one of the tasks fail
/// before all the tasks have been added. i.e. it is more suitable for
/// tasks that are limited. Here's a sample code where withThrowingTaskGroup
/// doesn't solve the problem:
///
/// ```
/// withThrowingTaskGroup { group in
/// for try await packet in receiver {
/// group.addTask {
/// /* process packet */
/// }
/// } /* this loop blocks forever waiting for more packets */
/// try await group.next() /* this never gets called */
/// }
/// ```
/// The above closure never returns even when a handler encounters an error
/// because the blocking operation `try await group.next()` cannot be
/// called while iterating over the receiver stream.
private func untilFirstError(body: @Sendable @escaping (UntilFirstError) async throws -> Void) async throws {
let group = try await UntilFirstError()
var taskContinuation: AsyncStream<Task<(), Error>>.Continuation?
let tasks = AsyncStream<Task<(), Error>> { continuation in
taskContinuation = continuation
}
guard let taskContinuation else {
throw NSError(
domain: "untilFirstError",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "Failed to initialize task continuation"])
}
defer { taskContinuation.finish() }
let stream = AsyncStream<Error> { continuation in
let processTasks = Task {
let taskStream = await group.tasks()
defer {
continuation.finish()
}
for await item in taskStream {
try Task.checkCancellation()
let addedTask = Task {
try Task.checkCancellation()
do {
try await item()
} catch {
continuation.yield(error)
await group.continuation?.finish()
throw error
}
}
taskContinuation.yield(addedTask)
}
}
taskContinuation.yield(processTasks)
let mainTask = Task { @Sendable in
defer {
continuation.finish()
processTasks.cancel()
taskContinuation.finish()
}
do {
try Task.checkCancellation()
try await body(group)
} catch {
continuation.yield(error)
await group.continuation?.finish()
throw error
}
}
taskContinuation.yield(mainTask)
}
// when the first handler fails, cancel all tasks and throw error
for await item in stream {
try Task.checkCancellation()
Task {
for await task in tasks {
task.cancel()
}
}
throw item
}
// if none of the handlers fail, wait for all subtasks to complete
for await task in tasks {
try Task.checkCancellation()
try await task.value
}
}
private actor UntilFirstError {
var stream: AsyncStream<@Sendable () async throws -> Void>?
var continuation: AsyncStream<@Sendable () async throws -> Void>.Continuation?
init() async throws {
self.stream = AsyncStream { cont in
self.continuation = cont
}
guard let _ = continuation else {
throw NSError()
}
}
func addTask(body: @Sendable @escaping () async throws -> Void) {
if !Task.isCancelled {
self.continuation?.yield(body)
}
}
func tasks() -> AsyncStream<@Sendable () async throws -> Void> {
self.stream!
}
}
}
@@ -0,0 +1,188 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerClient
import Containerization
import ContainerizationArchive
import ContainerizationOCI
import Foundation
import GRPC
struct BuildRemoteContentProxy: BuildPipelineHandler {
let local: ContentStore
public init(_ contentStore: ContentStore) throws {
self.local = contentStore
}
func accept(_ packet: ServerStream) throws -> Bool {
guard let imageTransfer = packet.getImageTransfer() else {
return false
}
guard imageTransfer.stage() == "content-store" else {
return false
}
return true
}
func handle(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ServerStream) async throws {
guard let imageTransfer = packet.getImageTransfer() else {
throw Error.imageTransferMissing
}
guard let method = imageTransfer.method() else {
throw Error.methodMissing
}
switch try ContentStoreMethod(method) {
case .info:
try await self.info(sender, imageTransfer, packet.buildID)
case .readerAt:
try await self.readerAt(sender, imageTransfer, packet.buildID)
default:
throw Error.unknownMethod(method)
}
}
func info(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ImageTransfer, _ buildID: String) async throws {
let descriptor = try await local.get(digest: packet.tag)
let size = try descriptor?.size()
let transfer = try ImageTransfer(
id: packet.id,
digest: packet.tag,
method: ContentStoreMethod.info.rawValue,
size: size
)
var response = ClientStream()
response.buildID = buildID
response.imageTransfer = transfer
response.packetType = .imageTransfer(transfer)
sender.yield(response)
}
func readerAt(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ImageTransfer, _ buildID: String) async throws {
let digest = packet.descriptor.digest
let offset: UInt64 = packet.offset() ?? 0
let size: Int = packet.len() ?? 0
guard let descriptor = try await local.get(digest: digest) else {
throw Error.contentMissing
}
if offset == 0 && size == 0 { // Metadata request
var transfer = try ImageTransfer(
id: packet.id,
digest: packet.tag,
method: ContentStoreMethod.readerAt.rawValue,
size: descriptor.size(),
data: Data()
)
transfer.complete = true
var response = ClientStream()
response.buildID = buildID
response.imageTransfer = transfer
response.packetType = .imageTransfer(transfer)
sender.yield(response)
return
}
guard let data = try descriptor.data(offset: offset, length: size) else {
throw Error.invalidOffsetSizeForContent(packet.descriptor.digest, offset, size)
}
let transfer = try ImageTransfer(
id: packet.id,
digest: packet.tag,
method: ContentStoreMethod.readerAt.rawValue,
size: UInt64(data.count),
data: data
)
var response = ClientStream()
response.buildID = buildID
response.imageTransfer = transfer
response.packetType = .imageTransfer(transfer)
sender.yield(response)
}
func delete(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ImageTransfer) async throws {
throw NSError(domain: "RemoteContentProxy", code: 1, userInfo: [NSLocalizedDescriptionKey: "unimplemented method \(ContentStoreMethod.delete)"])
}
func update(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ImageTransfer) async throws {
throw NSError(domain: "RemoteContentProxy", code: 1, userInfo: [NSLocalizedDescriptionKey: "unimplemented method \(ContentStoreMethod.update)"])
}
func walk(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ImageTransfer) async throws {
throw NSError(domain: "RemoteContentProxy", code: 1, userInfo: [NSLocalizedDescriptionKey: "unimplemented method \(ContentStoreMethod.walk)"])
}
enum ContentStoreMethod: String {
case info = "/containerd.services.content.v1.Content/Info"
case readerAt = "/containerd.services.content.v1.Content/ReaderAt"
case delete = "/containerd.services.content.v1.Content/Delete"
case update = "/containerd.services.content.v1.Content/Update"
case walk = "/containerd.services.content.v1.Content/Walk"
init(_ method: String) throws {
guard let value = ContentStoreMethod(rawValue: method) else {
throw Error.unknownMethod(method)
}
self = value
}
}
}
extension ImageTransfer {
fileprivate init(id: String, digest: String, method: String, size: UInt64? = nil, data: Data = Data()) throws {
self.init()
self.id = id
self.tag = digest
self.metadata = [
"os": "linux",
"stage": "content-store",
"method": method,
]
if let size {
self.metadata["size"] = String(size)
}
self.complete = true
self.direction = .into
self.data = data
}
}
extension BuildRemoteContentProxy {
enum Error: Swift.Error, CustomStringConvertible {
case imageTransferMissing
case methodMissing
case contentMissing
case unknownMethod(String)
case invalidOffsetSizeForContent(String, UInt64, Int)
var description: String {
switch self {
case .imageTransferMissing:
return "imageTransfer is missing"
case .methodMissing:
return "method is missing in request"
case .contentMissing:
return "content cannot be found"
case .unknownMethod(let m):
return "unknown content-store method \(m)"
case .invalidOffsetSizeForContent(let digest, let offset, let size):
return "invalid request for content: \(digest) with offset: \(offset) size: \(size)"
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationOS
import Foundation
import GRPC
import NIO
actor BuildStdio: BuildPipelineHandler {
public let quiet: Bool
public let handle: FileHandle
init(quiet: Bool = false, output: FileHandle = FileHandle.standardError) throws {
self.quiet = quiet
self.handle = output
}
nonisolated func accept(_ packet: ServerStream) throws -> Bool {
guard let _ = packet.getIO() else {
return false
}
return true
}
func handle(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ServerStream) async throws {
guard !quiet else {
return
}
guard let io = packet.getIO() else {
throw Error.ioMissing
}
if let cmdString = try TerminalCommand().json() {
var response = ClientStream()
response.buildID = packet.buildID
response.command = .init()
response.command.id = packet.buildID
response.command.command = cmdString
sender.yield(response)
}
handle.write(io.data)
}
}
extension BuildStdio {
enum Error: Swift.Error, CustomStringConvertible {
case ioMissing
case invalidContinuation
var description: String {
switch self {
case .ioMissing:
return "io field missing in packet"
case .invalidContinuation:
return "continuation could not created"
}
}
}
}
+881
View File
@@ -0,0 +1,881 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
//
// DO NOT EDIT.
// swift-format-ignore-file
//
// Generated by the protocol buffer compiler.
// Source: Builder.proto
//
import GRPC
import NIO
import NIOConcurrencyHelpers
import SwiftProtobuf
/// Builder service implements APIs for performing an image build with
/// Container image builder agent.
///
/// To perform a build:
///
/// 1. CreateBuild to create a new build
/// 2. StartBuild to start the build exection where client and server
/// both have a stream for exchanging data during the build.
///
/// The client may send:
/// a) signal packet to signal to the build process (e.g. SIGINT)
///
/// b) command packet for executing a command in the build file on the
/// server
/// NOTE: the server will need to switch on the command to determine the
/// type of command to execute (e.g. RUN, ENV, etc.)
///
/// c) transfer build data either to or from the server
/// - INTO direction is for sending build data to the server at specific
/// location (e.g. COPY)
/// - OUTOF direction is for copying build data from the server to be
/// used in subsequent build stages
///
/// d) transfer image content data either to or from the server
/// - INTO direction is for sending inherited image content data to the
/// server's local content store
/// - OUTOF direction is for copying successfully built OCI image from
/// the server to the client
///
/// The server may send:
/// a) stdio packet for the build progress
///
/// b) build error indicating unsuccessful build
///
/// c) command complete packet indicating a command has finished executing
///
/// d) handle transfer build data either to or from the client
///
/// e) handle transfer image content data either to or from the client
///
///
/// NOTE: The build data and image content data transfer is ALWAYS initiated
/// by the client.
///
/// Sequence for transferring from the client to the server:
/// 1. client send a BuildTransfer/ImageTransfer request with ID, direction
/// of 'INTO',
/// destination path, and first chunk of data
/// 2. server starts to receive the data and stream to a temporary file
/// 3. client continues to send all chunks of data until last chunk, which
/// client will
/// send with 'complete' set to true
/// 4. server continues to receive until the last chunk with 'complete' set
/// to true,
/// server will finish writing the last chunk and un-archive the
/// temporary file to the destination path
/// 5. server completes the transfer by sending a last
/// BuildTransfer/ImageTransfer with
/// 'complete' set to true
/// 6. client waits for the last BuildTransfer/ImageTransfer with 'complete'
/// set to true
/// before proceeding with the rest of the commands
///
/// Sequence for transferring from the server to the client:
/// 1. client send a BuildTransfer/ImageTransfer request with ID, direction
/// of 'OUTOF',
/// source path, and empty data
/// 2. server archives the data at source path, and starts to send chunks to
/// the client
/// 3. server coninues to send all chunks until last chunk, which server
/// will send with
/// 'complete' set to true
/// 4. client starts to receive the data and stream to a temporary file
/// 5. client continues to receive until the last chunk with 'complete' set
/// to true,
/// client will finish writing last chunk and un-archive the temporary
/// file to the destination path
/// 6. client MAY choose to send one last BuildTransfer/ImageTransfer with
/// 'complete'
/// set to true, but NOT required.
///
///
/// NOTE: the client should close the send stream once it has finished
/// receiving the build output or abadon the current build due to error.
/// Server should keep the stream open until it receives the EOF that client
/// has closed the stream, which the server should then close its send stream.
///
/// Usage: instantiate `Com_Apple_Container_Build_V1_BuilderClient`, then call methods of this protocol to make API calls.
public protocol Com_Apple_Container_Build_V1_BuilderClientProtocol: GRPCClient {
var serviceName: String { get }
var interceptors: Com_Apple_Container_Build_V1_BuilderClientInterceptorFactoryProtocol? { get }
func createBuild(
_ request: Com_Apple_Container_Build_V1_CreateBuildRequest,
callOptions: CallOptions?
) -> UnaryCall<Com_Apple_Container_Build_V1_CreateBuildRequest, Com_Apple_Container_Build_V1_CreateBuildResponse>
func performBuild(
callOptions: CallOptions?,
handler: @escaping (Com_Apple_Container_Build_V1_ServerStream) -> Void
) -> BidirectionalStreamingCall<Com_Apple_Container_Build_V1_ClientStream, Com_Apple_Container_Build_V1_ServerStream>
func info(
_ request: Com_Apple_Container_Build_V1_InfoRequest,
callOptions: CallOptions?
) -> UnaryCall<Com_Apple_Container_Build_V1_InfoRequest, Com_Apple_Container_Build_V1_InfoResponse>
}
extension Com_Apple_Container_Build_V1_BuilderClientProtocol {
public var serviceName: String {
return "com.apple.container.build.v1.Builder"
}
/// Create a build request.
///
/// - Parameters:
/// - request: Request to send to CreateBuild.
/// - callOptions: Call options.
/// - Returns: A `UnaryCall` with futures for the metadata, status and response.
public func createBuild(
_ request: Com_Apple_Container_Build_V1_CreateBuildRequest,
callOptions: CallOptions? = nil
) -> UnaryCall<Com_Apple_Container_Build_V1_CreateBuildRequest, Com_Apple_Container_Build_V1_CreateBuildResponse> {
return self.makeUnaryCall(
path: Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.createBuild.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeCreateBuildInterceptors() ?? []
)
}
/// Perform the build.
/// Executes the entire build sequence with attaching input/output
/// to handling data exchange with the server during the build.
///
/// Callers should use the `send` method on the returned object to send messages
/// to the server. The caller should send an `.end` after the final message has been sent.
///
/// - Parameters:
/// - callOptions: Call options.
/// - handler: A closure called when each response is received from the server.
/// - Returns: A `ClientStreamingCall` with futures for the metadata and status.
public func performBuild(
callOptions: CallOptions? = nil,
handler: @escaping (Com_Apple_Container_Build_V1_ServerStream) -> Void
) -> BidirectionalStreamingCall<Com_Apple_Container_Build_V1_ClientStream, Com_Apple_Container_Build_V1_ServerStream> {
return self.makeBidirectionalStreamingCall(
path: Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.performBuild.path,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makePerformBuildInterceptors() ?? [],
handler: handler
)
}
/// Unary call to Info
///
/// - Parameters:
/// - request: Request to send to Info.
/// - callOptions: Call options.
/// - Returns: A `UnaryCall` with futures for the metadata, status and response.
public func info(
_ request: Com_Apple_Container_Build_V1_InfoRequest,
callOptions: CallOptions? = nil
) -> UnaryCall<Com_Apple_Container_Build_V1_InfoRequest, Com_Apple_Container_Build_V1_InfoResponse> {
return self.makeUnaryCall(
path: Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.info.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeInfoInterceptors() ?? []
)
}
}
@available(*, deprecated)
extension Com_Apple_Container_Build_V1_BuilderClient: @unchecked Sendable {}
@available(*, deprecated, renamed: "Com_Apple_Container_Build_V1_BuilderNIOClient")
public final class Com_Apple_Container_Build_V1_BuilderClient: Com_Apple_Container_Build_V1_BuilderClientProtocol {
private let lock = Lock()
private var _defaultCallOptions: CallOptions
private var _interceptors: Com_Apple_Container_Build_V1_BuilderClientInterceptorFactoryProtocol?
public let channel: GRPCChannel
public var defaultCallOptions: CallOptions {
get { self.lock.withLock { return self._defaultCallOptions } }
set { self.lock.withLockVoid { self._defaultCallOptions = newValue } }
}
public var interceptors: Com_Apple_Container_Build_V1_BuilderClientInterceptorFactoryProtocol? {
get { self.lock.withLock { return self._interceptors } }
set { self.lock.withLockVoid { self._interceptors = newValue } }
}
/// Creates a client for the com.apple.container.build.v1.Builder service.
///
/// - Parameters:
/// - channel: `GRPCChannel` to the service host.
/// - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
/// - interceptors: A factory providing interceptors for each RPC.
public init(
channel: GRPCChannel,
defaultCallOptions: CallOptions = CallOptions(),
interceptors: Com_Apple_Container_Build_V1_BuilderClientInterceptorFactoryProtocol? = nil
) {
self.channel = channel
self._defaultCallOptions = defaultCallOptions
self._interceptors = interceptors
}
}
public struct Com_Apple_Container_Build_V1_BuilderNIOClient: Com_Apple_Container_Build_V1_BuilderClientProtocol {
public var channel: GRPCChannel
public var defaultCallOptions: CallOptions
public var interceptors: Com_Apple_Container_Build_V1_BuilderClientInterceptorFactoryProtocol?
/// Creates a client for the com.apple.container.build.v1.Builder service.
///
/// - Parameters:
/// - channel: `GRPCChannel` to the service host.
/// - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
/// - interceptors: A factory providing interceptors for each RPC.
public init(
channel: GRPCChannel,
defaultCallOptions: CallOptions = CallOptions(),
interceptors: Com_Apple_Container_Build_V1_BuilderClientInterceptorFactoryProtocol? = nil
) {
self.channel = channel
self.defaultCallOptions = defaultCallOptions
self.interceptors = interceptors
}
}
/// Builder service implements APIs for performing an image build with
/// Container image builder agent.
///
/// To perform a build:
///
/// 1. CreateBuild to create a new build
/// 2. StartBuild to start the build exection where client and server
/// both have a stream for exchanging data during the build.
///
/// The client may send:
/// a) signal packet to signal to the build process (e.g. SIGINT)
///
/// b) command packet for executing a command in the build file on the
/// server
/// NOTE: the server will need to switch on the command to determine the
/// type of command to execute (e.g. RUN, ENV, etc.)
///
/// c) transfer build data either to or from the server
/// - INTO direction is for sending build data to the server at specific
/// location (e.g. COPY)
/// - OUTOF direction is for copying build data from the server to be
/// used in subsequent build stages
///
/// d) transfer image content data either to or from the server
/// - INTO direction is for sending inherited image content data to the
/// server's local content store
/// - OUTOF direction is for copying successfully built OCI image from
/// the server to the client
///
/// The server may send:
/// a) stdio packet for the build progress
///
/// b) build error indicating unsuccessful build
///
/// c) command complete packet indicating a command has finished executing
///
/// d) handle transfer build data either to or from the client
///
/// e) handle transfer image content data either to or from the client
///
///
/// NOTE: The build data and image content data transfer is ALWAYS initiated
/// by the client.
///
/// Sequence for transferring from the client to the server:
/// 1. client send a BuildTransfer/ImageTransfer request with ID, direction
/// of 'INTO',
/// destination path, and first chunk of data
/// 2. server starts to receive the data and stream to a temporary file
/// 3. client continues to send all chunks of data until last chunk, which
/// client will
/// send with 'complete' set to true
/// 4. server continues to receive until the last chunk with 'complete' set
/// to true,
/// server will finish writing the last chunk and un-archive the
/// temporary file to the destination path
/// 5. server completes the transfer by sending a last
/// BuildTransfer/ImageTransfer with
/// 'complete' set to true
/// 6. client waits for the last BuildTransfer/ImageTransfer with 'complete'
/// set to true
/// before proceeding with the rest of the commands
///
/// Sequence for transferring from the server to the client:
/// 1. client send a BuildTransfer/ImageTransfer request with ID, direction
/// of 'OUTOF',
/// source path, and empty data
/// 2. server archives the data at source path, and starts to send chunks to
/// the client
/// 3. server coninues to send all chunks until last chunk, which server
/// will send with
/// 'complete' set to true
/// 4. client starts to receive the data and stream to a temporary file
/// 5. client continues to receive until the last chunk with 'complete' set
/// to true,
/// client will finish writing last chunk and un-archive the temporary
/// file to the destination path
/// 6. client MAY choose to send one last BuildTransfer/ImageTransfer with
/// 'complete'
/// set to true, but NOT required.
///
///
/// NOTE: the client should close the send stream once it has finished
/// receiving the build output or abadon the current build due to error.
/// Server should keep the stream open until it receives the EOF that client
/// has closed the stream, which the server should then close its send stream.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public protocol Com_Apple_Container_Build_V1_BuilderAsyncClientProtocol: GRPCClient {
static var serviceDescriptor: GRPCServiceDescriptor { get }
var interceptors: Com_Apple_Container_Build_V1_BuilderClientInterceptorFactoryProtocol? { get }
func makeCreateBuildCall(
_ request: Com_Apple_Container_Build_V1_CreateBuildRequest,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<Com_Apple_Container_Build_V1_CreateBuildRequest, Com_Apple_Container_Build_V1_CreateBuildResponse>
func makePerformBuildCall(
callOptions: CallOptions?
) -> GRPCAsyncBidirectionalStreamingCall<Com_Apple_Container_Build_V1_ClientStream, Com_Apple_Container_Build_V1_ServerStream>
func makeInfoCall(
_ request: Com_Apple_Container_Build_V1_InfoRequest,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<Com_Apple_Container_Build_V1_InfoRequest, Com_Apple_Container_Build_V1_InfoResponse>
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension Com_Apple_Container_Build_V1_BuilderAsyncClientProtocol {
public static var serviceDescriptor: GRPCServiceDescriptor {
return Com_Apple_Container_Build_V1_BuilderClientMetadata.serviceDescriptor
}
public var interceptors: Com_Apple_Container_Build_V1_BuilderClientInterceptorFactoryProtocol? {
return nil
}
public func makeCreateBuildCall(
_ request: Com_Apple_Container_Build_V1_CreateBuildRequest,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<Com_Apple_Container_Build_V1_CreateBuildRequest, Com_Apple_Container_Build_V1_CreateBuildResponse> {
return self.makeAsyncUnaryCall(
path: Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.createBuild.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeCreateBuildInterceptors() ?? []
)
}
public func makePerformBuildCall(
callOptions: CallOptions? = nil
) -> GRPCAsyncBidirectionalStreamingCall<Com_Apple_Container_Build_V1_ClientStream, Com_Apple_Container_Build_V1_ServerStream> {
return self.makeAsyncBidirectionalStreamingCall(
path: Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.performBuild.path,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makePerformBuildInterceptors() ?? []
)
}
public func makeInfoCall(
_ request: Com_Apple_Container_Build_V1_InfoRequest,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<Com_Apple_Container_Build_V1_InfoRequest, Com_Apple_Container_Build_V1_InfoResponse> {
return self.makeAsyncUnaryCall(
path: Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.info.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeInfoInterceptors() ?? []
)
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension Com_Apple_Container_Build_V1_BuilderAsyncClientProtocol {
public func createBuild(
_ request: Com_Apple_Container_Build_V1_CreateBuildRequest,
callOptions: CallOptions? = nil
) async throws -> Com_Apple_Container_Build_V1_CreateBuildResponse {
return try await self.performAsyncUnaryCall(
path: Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.createBuild.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeCreateBuildInterceptors() ?? []
)
}
public func performBuild<RequestStream>(
_ requests: RequestStream,
callOptions: CallOptions? = nil
) -> GRPCAsyncResponseStream<Com_Apple_Container_Build_V1_ServerStream> where RequestStream: Sequence, RequestStream.Element == Com_Apple_Container_Build_V1_ClientStream {
return self.performAsyncBidirectionalStreamingCall(
path: Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.performBuild.path,
requests: requests,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makePerformBuildInterceptors() ?? []
)
}
public func performBuild<RequestStream>(
_ requests: RequestStream,
callOptions: CallOptions? = nil
) -> GRPCAsyncResponseStream<Com_Apple_Container_Build_V1_ServerStream> where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Com_Apple_Container_Build_V1_ClientStream {
return self.performAsyncBidirectionalStreamingCall(
path: Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.performBuild.path,
requests: requests,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makePerformBuildInterceptors() ?? []
)
}
public func info(
_ request: Com_Apple_Container_Build_V1_InfoRequest,
callOptions: CallOptions? = nil
) async throws -> Com_Apple_Container_Build_V1_InfoResponse {
return try await self.performAsyncUnaryCall(
path: Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.info.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeInfoInterceptors() ?? []
)
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public struct Com_Apple_Container_Build_V1_BuilderAsyncClient: Com_Apple_Container_Build_V1_BuilderAsyncClientProtocol {
public var channel: GRPCChannel
public var defaultCallOptions: CallOptions
public var interceptors: Com_Apple_Container_Build_V1_BuilderClientInterceptorFactoryProtocol?
public init(
channel: GRPCChannel,
defaultCallOptions: CallOptions = CallOptions(),
interceptors: Com_Apple_Container_Build_V1_BuilderClientInterceptorFactoryProtocol? = nil
) {
self.channel = channel
self.defaultCallOptions = defaultCallOptions
self.interceptors = interceptors
}
}
public protocol Com_Apple_Container_Build_V1_BuilderClientInterceptorFactoryProtocol: Sendable {
/// - Returns: Interceptors to use when invoking 'createBuild'.
func makeCreateBuildInterceptors() -> [ClientInterceptor<Com_Apple_Container_Build_V1_CreateBuildRequest, Com_Apple_Container_Build_V1_CreateBuildResponse>]
/// - Returns: Interceptors to use when invoking 'performBuild'.
func makePerformBuildInterceptors() -> [ClientInterceptor<Com_Apple_Container_Build_V1_ClientStream, Com_Apple_Container_Build_V1_ServerStream>]
/// - Returns: Interceptors to use when invoking 'info'.
func makeInfoInterceptors() -> [ClientInterceptor<Com_Apple_Container_Build_V1_InfoRequest, Com_Apple_Container_Build_V1_InfoResponse>]
}
public enum Com_Apple_Container_Build_V1_BuilderClientMetadata {
public static let serviceDescriptor = GRPCServiceDescriptor(
name: "Builder",
fullName: "com.apple.container.build.v1.Builder",
methods: [
Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.createBuild,
Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.performBuild,
Com_Apple_Container_Build_V1_BuilderClientMetadata.Methods.info,
]
)
public enum Methods {
public static let createBuild = GRPCMethodDescriptor(
name: "CreateBuild",
path: "/com.apple.container.build.v1.Builder/CreateBuild",
type: GRPCCallType.unary
)
public static let performBuild = GRPCMethodDescriptor(
name: "PerformBuild",
path: "/com.apple.container.build.v1.Builder/PerformBuild",
type: GRPCCallType.bidirectionalStreaming
)
public static let info = GRPCMethodDescriptor(
name: "Info",
path: "/com.apple.container.build.v1.Builder/Info",
type: GRPCCallType.unary
)
}
}
/// Builder service implements APIs for performing an image build with
/// Container image builder agent.
///
/// To perform a build:
///
/// 1. CreateBuild to create a new build
/// 2. StartBuild to start the build exection where client and server
/// both have a stream for exchanging data during the build.
///
/// The client may send:
/// a) signal packet to signal to the build process (e.g. SIGINT)
///
/// b) command packet for executing a command in the build file on the
/// server
/// NOTE: the server will need to switch on the command to determine the
/// type of command to execute (e.g. RUN, ENV, etc.)
///
/// c) transfer build data either to or from the server
/// - INTO direction is for sending build data to the server at specific
/// location (e.g. COPY)
/// - OUTOF direction is for copying build data from the server to be
/// used in subsequent build stages
///
/// d) transfer image content data either to or from the server
/// - INTO direction is for sending inherited image content data to the
/// server's local content store
/// - OUTOF direction is for copying successfully built OCI image from
/// the server to the client
///
/// The server may send:
/// a) stdio packet for the build progress
///
/// b) build error indicating unsuccessful build
///
/// c) command complete packet indicating a command has finished executing
///
/// d) handle transfer build data either to or from the client
///
/// e) handle transfer image content data either to or from the client
///
///
/// NOTE: The build data and image content data transfer is ALWAYS initiated
/// by the client.
///
/// Sequence for transferring from the client to the server:
/// 1. client send a BuildTransfer/ImageTransfer request with ID, direction
/// of 'INTO',
/// destination path, and first chunk of data
/// 2. server starts to receive the data and stream to a temporary file
/// 3. client continues to send all chunks of data until last chunk, which
/// client will
/// send with 'complete' set to true
/// 4. server continues to receive until the last chunk with 'complete' set
/// to true,
/// server will finish writing the last chunk and un-archive the
/// temporary file to the destination path
/// 5. server completes the transfer by sending a last
/// BuildTransfer/ImageTransfer with
/// 'complete' set to true
/// 6. client waits for the last BuildTransfer/ImageTransfer with 'complete'
/// set to true
/// before proceeding with the rest of the commands
///
/// Sequence for transferring from the server to the client:
/// 1. client send a BuildTransfer/ImageTransfer request with ID, direction
/// of 'OUTOF',
/// source path, and empty data
/// 2. server archives the data at source path, and starts to send chunks to
/// the client
/// 3. server coninues to send all chunks until last chunk, which server
/// will send with
/// 'complete' set to true
/// 4. client starts to receive the data and stream to a temporary file
/// 5. client continues to receive until the last chunk with 'complete' set
/// to true,
/// client will finish writing last chunk and un-archive the temporary
/// file to the destination path
/// 6. client MAY choose to send one last BuildTransfer/ImageTransfer with
/// 'complete'
/// set to true, but NOT required.
///
///
/// NOTE: the client should close the send stream once it has finished
/// receiving the build output or abadon the current build due to error.
/// Server should keep the stream open until it receives the EOF that client
/// has closed the stream, which the server should then close its send stream.
///
/// To build a server, implement a class that conforms to this protocol.
public protocol Com_Apple_Container_Build_V1_BuilderProvider: CallHandlerProvider {
var interceptors: Com_Apple_Container_Build_V1_BuilderServerInterceptorFactoryProtocol? { get }
/// Create a build request.
func createBuild(request: Com_Apple_Container_Build_V1_CreateBuildRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Com_Apple_Container_Build_V1_CreateBuildResponse>
/// Perform the build.
/// Executes the entire build sequence with attaching input/output
/// to handling data exchange with the server during the build.
func performBuild(context: StreamingResponseCallContext<Com_Apple_Container_Build_V1_ServerStream>) -> EventLoopFuture<(StreamEvent<Com_Apple_Container_Build_V1_ClientStream>) -> Void>
func info(request: Com_Apple_Container_Build_V1_InfoRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Com_Apple_Container_Build_V1_InfoResponse>
}
extension Com_Apple_Container_Build_V1_BuilderProvider {
public var serviceName: Substring {
return Com_Apple_Container_Build_V1_BuilderServerMetadata.serviceDescriptor.fullName[...]
}
/// Determines, calls and returns the appropriate request handler, depending on the request's method.
/// Returns nil for methods not handled by this service.
public func handle(
method name: Substring,
context: CallHandlerContext
) -> GRPCServerHandlerProtocol? {
switch name {
case "CreateBuild":
return UnaryServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Container_Build_V1_CreateBuildRequest>(),
responseSerializer: ProtobufSerializer<Com_Apple_Container_Build_V1_CreateBuildResponse>(),
interceptors: self.interceptors?.makeCreateBuildInterceptors() ?? [],
userFunction: self.createBuild(request:context:)
)
case "PerformBuild":
return BidirectionalStreamingServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Container_Build_V1_ClientStream>(),
responseSerializer: ProtobufSerializer<Com_Apple_Container_Build_V1_ServerStream>(),
interceptors: self.interceptors?.makePerformBuildInterceptors() ?? [],
observerFactory: self.performBuild(context:)
)
case "Info":
return UnaryServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Container_Build_V1_InfoRequest>(),
responseSerializer: ProtobufSerializer<Com_Apple_Container_Build_V1_InfoResponse>(),
interceptors: self.interceptors?.makeInfoInterceptors() ?? [],
userFunction: self.info(request:context:)
)
default:
return nil
}
}
}
/// Builder service implements APIs for performing an image build with
/// Container image builder agent.
///
/// To perform a build:
///
/// 1. CreateBuild to create a new build
/// 2. StartBuild to start the build exection where client and server
/// both have a stream for exchanging data during the build.
///
/// The client may send:
/// a) signal packet to signal to the build process (e.g. SIGINT)
///
/// b) command packet for executing a command in the build file on the
/// server
/// NOTE: the server will need to switch on the command to determine the
/// type of command to execute (e.g. RUN, ENV, etc.)
///
/// c) transfer build data either to or from the server
/// - INTO direction is for sending build data to the server at specific
/// location (e.g. COPY)
/// - OUTOF direction is for copying build data from the server to be
/// used in subsequent build stages
///
/// d) transfer image content data either to or from the server
/// - INTO direction is for sending inherited image content data to the
/// server's local content store
/// - OUTOF direction is for copying successfully built OCI image from
/// the server to the client
///
/// The server may send:
/// a) stdio packet for the build progress
///
/// b) build error indicating unsuccessful build
///
/// c) command complete packet indicating a command has finished executing
///
/// d) handle transfer build data either to or from the client
///
/// e) handle transfer image content data either to or from the client
///
///
/// NOTE: The build data and image content data transfer is ALWAYS initiated
/// by the client.
///
/// Sequence for transferring from the client to the server:
/// 1. client send a BuildTransfer/ImageTransfer request with ID, direction
/// of 'INTO',
/// destination path, and first chunk of data
/// 2. server starts to receive the data and stream to a temporary file
/// 3. client continues to send all chunks of data until last chunk, which
/// client will
/// send with 'complete' set to true
/// 4. server continues to receive until the last chunk with 'complete' set
/// to true,
/// server will finish writing the last chunk and un-archive the
/// temporary file to the destination path
/// 5. server completes the transfer by sending a last
/// BuildTransfer/ImageTransfer with
/// 'complete' set to true
/// 6. client waits for the last BuildTransfer/ImageTransfer with 'complete'
/// set to true
/// before proceeding with the rest of the commands
///
/// Sequence for transferring from the server to the client:
/// 1. client send a BuildTransfer/ImageTransfer request with ID, direction
/// of 'OUTOF',
/// source path, and empty data
/// 2. server archives the data at source path, and starts to send chunks to
/// the client
/// 3. server coninues to send all chunks until last chunk, which server
/// will send with
/// 'complete' set to true
/// 4. client starts to receive the data and stream to a temporary file
/// 5. client continues to receive until the last chunk with 'complete' set
/// to true,
/// client will finish writing last chunk and un-archive the temporary
/// file to the destination path
/// 6. client MAY choose to send one last BuildTransfer/ImageTransfer with
/// 'complete'
/// set to true, but NOT required.
///
///
/// NOTE: the client should close the send stream once it has finished
/// receiving the build output or abadon the current build due to error.
/// Server should keep the stream open until it receives the EOF that client
/// has closed the stream, which the server should then close its send stream.
///
/// To implement a server, implement an object which conforms to this protocol.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public protocol Com_Apple_Container_Build_V1_BuilderAsyncProvider: CallHandlerProvider, Sendable {
static var serviceDescriptor: GRPCServiceDescriptor { get }
var interceptors: Com_Apple_Container_Build_V1_BuilderServerInterceptorFactoryProtocol? { get }
/// Create a build request.
func createBuild(
request: Com_Apple_Container_Build_V1_CreateBuildRequest,
context: GRPCAsyncServerCallContext
) async throws -> Com_Apple_Container_Build_V1_CreateBuildResponse
/// Perform the build.
/// Executes the entire build sequence with attaching input/output
/// to handling data exchange with the server during the build.
func performBuild(
requestStream: GRPCAsyncRequestStream<Com_Apple_Container_Build_V1_ClientStream>,
responseStream: GRPCAsyncResponseStreamWriter<Com_Apple_Container_Build_V1_ServerStream>,
context: GRPCAsyncServerCallContext
) async throws
func info(
request: Com_Apple_Container_Build_V1_InfoRequest,
context: GRPCAsyncServerCallContext
) async throws -> Com_Apple_Container_Build_V1_InfoResponse
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension Com_Apple_Container_Build_V1_BuilderAsyncProvider {
public static var serviceDescriptor: GRPCServiceDescriptor {
return Com_Apple_Container_Build_V1_BuilderServerMetadata.serviceDescriptor
}
public var serviceName: Substring {
return Com_Apple_Container_Build_V1_BuilderServerMetadata.serviceDescriptor.fullName[...]
}
public var interceptors: Com_Apple_Container_Build_V1_BuilderServerInterceptorFactoryProtocol? {
return nil
}
public func handle(
method name: Substring,
context: CallHandlerContext
) -> GRPCServerHandlerProtocol? {
switch name {
case "CreateBuild":
return GRPCAsyncServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Container_Build_V1_CreateBuildRequest>(),
responseSerializer: ProtobufSerializer<Com_Apple_Container_Build_V1_CreateBuildResponse>(),
interceptors: self.interceptors?.makeCreateBuildInterceptors() ?? [],
wrapping: { try await self.createBuild(request: $0, context: $1) }
)
case "PerformBuild":
return GRPCAsyncServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Container_Build_V1_ClientStream>(),
responseSerializer: ProtobufSerializer<Com_Apple_Container_Build_V1_ServerStream>(),
interceptors: self.interceptors?.makePerformBuildInterceptors() ?? [],
wrapping: { try await self.performBuild(requestStream: $0, responseStream: $1, context: $2) }
)
case "Info":
return GRPCAsyncServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Container_Build_V1_InfoRequest>(),
responseSerializer: ProtobufSerializer<Com_Apple_Container_Build_V1_InfoResponse>(),
interceptors: self.interceptors?.makeInfoInterceptors() ?? [],
wrapping: { try await self.info(request: $0, context: $1) }
)
default:
return nil
}
}
}
public protocol Com_Apple_Container_Build_V1_BuilderServerInterceptorFactoryProtocol: Sendable {
/// - Returns: Interceptors to use when handling 'createBuild'.
/// Defaults to calling `self.makeInterceptors()`.
func makeCreateBuildInterceptors() -> [ServerInterceptor<Com_Apple_Container_Build_V1_CreateBuildRequest, Com_Apple_Container_Build_V1_CreateBuildResponse>]
/// - Returns: Interceptors to use when handling 'performBuild'.
/// Defaults to calling `self.makeInterceptors()`.
func makePerformBuildInterceptors() -> [ServerInterceptor<Com_Apple_Container_Build_V1_ClientStream, Com_Apple_Container_Build_V1_ServerStream>]
/// - Returns: Interceptors to use when handling 'info'.
/// Defaults to calling `self.makeInterceptors()`.
func makeInfoInterceptors() -> [ServerInterceptor<Com_Apple_Container_Build_V1_InfoRequest, Com_Apple_Container_Build_V1_InfoResponse>]
}
public enum Com_Apple_Container_Build_V1_BuilderServerMetadata {
public static let serviceDescriptor = GRPCServiceDescriptor(
name: "Builder",
fullName: "com.apple.container.build.v1.Builder",
methods: [
Com_Apple_Container_Build_V1_BuilderServerMetadata.Methods.createBuild,
Com_Apple_Container_Build_V1_BuilderServerMetadata.Methods.performBuild,
Com_Apple_Container_Build_V1_BuilderServerMetadata.Methods.info,
]
)
public enum Methods {
public static let createBuild = GRPCMethodDescriptor(
name: "CreateBuild",
path: "/com.apple.container.build.v1.Builder/CreateBuild",
type: GRPCCallType.unary
)
public static let performBuild = GRPCMethodDescriptor(
name: "PerformBuild",
path: "/com.apple.container.build.v1.Builder/PerformBuild",
type: GRPCCallType.bidirectionalStreaming
)
public static let info = GRPCMethodDescriptor(
name: "Info",
path: "/com.apple.container.build.v1.Builder/Info",
type: GRPCCallType.unary
)
}
}
File diff suppressed because it is too large Load Diff
+383
View File
@@ -0,0 +1,383 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerClient
import Containerization
import ContainerizationOCI
import ContainerizationOS
import Foundation
import GRPC
import NIO
import NIOHPACK
import NIOHTTP2
public struct Builder: Sendable {
let client: BuilderClientProtocol
let clientAsync: BuilderClientAsyncProtocol
let group: EventLoopGroup
let builderShimSocket: FileHandle
let channel: GRPCChannel
public init(socket: FileHandle, group: EventLoopGroup) throws {
try socket.setSendBufSize(4 << 20)
try socket.setRecvBufSize(2 << 20)
var config = ClientConnection.Configuration.default(
target: .connectedSocket(socket.fileDescriptor),
eventLoopGroup: group
)
config.connectionIdleTimeout = TimeAmount(.seconds(600))
config.connectionKeepalive = .init(
interval: TimeAmount(.seconds(600)),
timeout: TimeAmount(.seconds(500)),
permitWithoutCalls: true
)
config.connectionBackoff = .init(
initialBackoff: TimeInterval(1),
maximumBackoff: TimeInterval(10)
)
config.callStartBehavior = .fastFailure
config.httpMaxFrameSize = 8 << 10
config.maximumReceiveMessageLength = 512 << 20
config.httpTargetWindowSize = 16 << 10
let channel = ClientConnection(configuration: config)
self.channel = channel
self.clientAsync = BuilderClientAsync(channel: channel)
self.client = BuilderClient(channel: channel)
self.group = group
self.builderShimSocket = socket
}
public func info() throws -> InfoResponse {
let resp = self.client.info(InfoRequest(), callOptions: CallOptions())
return try resp.response.wait()
}
public func info() async throws -> InfoResponse {
let opts = CallOptions(timeLimit: .timeout(.seconds(30)))
return try await self.clientAsync.info(InfoRequest(), callOptions: opts)
}
// TODO
// - Symlinks in build context dir
// - cache-to, cache-from
// - output (other than the default OCI image output, e.g., local, tar, Docker)
public func build(_ config: BuildConfig) async throws {
var continuation: AsyncStream<ClientStream>.Continuation?
let reqStream = AsyncStream<ClientStream> { (cont: AsyncStream<ClientStream>.Continuation) in
continuation = cont
}
guard let continuation else {
throw Error.invalidContinuation
}
defer {
continuation.finish()
}
if let terminal = config.terminal {
Task {
let winchHandler = AsyncSignalHandler.create(notify: [SIGWINCH])
let setWinch = { (rows: UInt16, cols: UInt16) in
var winch = ClientStream()
winch.command = .init()
if let cmdString = try TerminalCommand(rows: rows, cols: cols).json() {
winch.command.command = cmdString
continuation.yield(winch)
}
}
let size = try terminal.size
var width = size.width
var height = size.height
try setWinch(height, width)
for await _ in winchHandler.signals {
let size = try terminal.size
let cols = size.width
let rows = size.height
if cols != width || rows != height {
width = cols
height = rows
try setWinch(height, width)
}
}
}
}
let respStream = self.clientAsync.performBuild(reqStream, callOptions: try CallOptions(config))
let pipeline = try await BuildPipeline(config)
do {
try await pipeline.run(sender: continuation, receiver: respStream)
} catch Error.buildComplete {
_ = channel.close()
try await group.shutdownGracefully()
return
}
}
public struct BuildExport: Sendable {
public let type: String
public var destination: URL?
public let additionalFields: [String: String]
public let rawValue: String
public init(type: String, destination: URL?, additionalFields: [String: String], rawValue: String) {
self.type = type
self.destination = destination
self.additionalFields = additionalFields
self.rawValue = rawValue
}
public init(from input: String) throws {
var typeValue: String?
var destinationValue: URL?
var additionalFields: [String: String] = [:]
let pairs = input.components(separatedBy: ",")
for pair in pairs {
let parts = pair.components(separatedBy: "=")
guard parts.count == 2 else { continue }
let key = parts[0].trimmingCharacters(in: .whitespaces)
let value = parts[1].trimmingCharacters(in: .whitespaces)
switch key {
case "type":
typeValue = value
case "dest":
destinationValue = try Self.resolveDestination(dest: value)
default:
additionalFields[key] = value
}
}
guard let type = typeValue else {
throw Builder.Error.invalidExport(input, "type field is required")
}
switch type {
case "oci":
break
case "tar":
if destinationValue == nil {
throw Builder.Error.invalidExport(input, "dest field is required")
}
default:
throw Builder.Error.invalidExport(input, "unsupported output type")
}
self.init(type: type, destination: destinationValue, additionalFields: additionalFields, rawValue: input)
}
public var stringValue: String {
get throws {
var components = ["type=\(type)"]
switch type {
case "oci", "tar":
break // ignore destination
default:
throw Builder.Error.invalidExport(rawValue, "unsupported output type")
}
for (key, value) in additionalFields {
components.append("\(key)=\(value)")
}
return components.joined(separator: ",")
}
}
static func resolveDestination(dest: String) throws -> URL {
let destination = URL(fileURLWithPath: dest)
let fileManager = FileManager.default
if fileManager.fileExists(atPath: destination.path) {
let resourceValues = try destination.resourceValues(forKeys: [.isDirectoryKey])
let isDir = resourceValues.isDirectory
if isDir != nil && isDir == false {
throw Builder.Error.invalidExport(dest, "dest path already exists")
}
var finalDestination = destination.appendingPathComponent("out.tar")
var index = 1
while fileManager.fileExists(atPath: finalDestination.path) {
let path = "out.tar.\(index)"
finalDestination = destination.appendingPathComponent(path)
index += 1
}
return finalDestination
} else {
let parentDirectory = destination.deletingLastPathComponent()
try? fileManager.createDirectory(at: parentDirectory, withIntermediateDirectories: true, attributes: nil)
}
return destination
}
}
public struct BuildConfig: Sendable {
public let buildID: String
public let contentStore: ContentStore
public let buildArgs: [String]
public let contextDir: String
public let dockerfile: Data
public let labels: [String]
public let noCache: Bool
public let platforms: [Platform]
public let terminal: Terminal?
public let tag: String
public let target: String
public let quiet: Bool
public let exports: [BuildExport]
public let cacheIn: [String]
public let cacheOut: [String]
public init(
buildID: String,
contentStore: ContentStore,
buildArgs: [String],
contextDir: String,
dockerfile: Data,
labels: [String],
noCache: Bool,
platforms: [Platform],
terminal: Terminal?,
tag: String,
target: String,
quiet: Bool,
exports: [BuildExport],
cacheIn: [String],
cacheOut: [String],
) {
self.buildID = buildID
self.contentStore = contentStore
self.buildArgs = buildArgs
self.contextDir = contextDir
self.dockerfile = dockerfile
self.labels = labels
self.noCache = noCache
self.platforms = platforms
self.terminal = terminal
self.tag = tag
self.target = target
self.quiet = quiet
self.exports = exports
self.cacheIn = cacheIn
self.cacheOut = cacheOut
}
}
}
extension Builder {
enum Error: Swift.Error, CustomStringConvertible {
case invalidContinuation
case buildComplete
case invalidExport(String, String)
var description: String {
switch self {
case .invalidContinuation:
return "continuation could not created"
case .buildComplete:
return "build completed"
case .invalidExport(let exp, let reason):
return "export entry \(exp) is invalid: \(reason)"
}
}
}
}
extension CallOptions {
public init(_ config: Builder.BuildConfig) throws {
var headers: [(String, String)] = [
("build-id", config.buildID),
("context", URL(filePath: config.contextDir).path(percentEncoded: false)),
("dockerfile", config.dockerfile.base64EncodedString()),
("progress", config.terminal != nil ? "tty" : "plain"),
("tag", config.tag),
("target", config.target),
]
for platform in config.platforms {
headers.append(("platforms", platform.description))
}
if config.noCache {
headers.append(("no-cache", ""))
}
for label in config.labels {
headers.append(("labels", label))
}
for buildArg in config.buildArgs {
headers.append(("build-args", buildArg))
}
for output in config.exports {
headers.append(("outputs", try output.stringValue))
}
for cacheIn in config.cacheIn {
headers.append(("cache-in", cacheIn))
}
for cacheOut in config.cacheOut {
headers.append(("cache-out", cacheOut))
}
self.init(
customMetadata: HPACKHeaders(headers)
)
}
}
extension FileHandle {
@discardableResult
func setSendBufSize(_ bytes: Int) throws -> Int {
try setSockOpt(
level: SOL_SOCKET,
name: SO_SNDBUF,
value: bytes)
return bytes
}
@discardableResult
func setRecvBufSize(_ bytes: Int) throws -> Int {
try setSockOpt(
level: SOL_SOCKET,
name: SO_RCVBUF,
value: bytes)
return bytes
}
private func setSockOpt(level: Int32, name: Int32, value: Int) throws {
var v = Int32(value)
let res = withUnsafePointer(to: &v) { ptr -> Int32 in
ptr.withMemoryRebound(
to: UInt8.self,
capacity: MemoryLayout<Int32>.size
) { raw in
#if canImport(Darwin)
return setsockopt(
self.fileDescriptor,
level, name,
raw,
socklen_t(MemoryLayout<Int32>.size))
#else
fatalError("unsupported platform")
#endif
}
}
if res == -1 {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EPERM)
}
}
}
+121
View File
@@ -0,0 +1,121 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
public class Globber {
let input: URL
var results: Set<URL> = .init()
public init(_ input: URL) {
self.input = input
}
public func match(_ pattern: String) throws {
let adjustedPattern =
pattern
.replacingOccurrences(of: #"^\./(?=.)"#, with: "", options: .regularExpression)
.replacingOccurrences(of: "^\\.[/]?$", with: "*", options: .regularExpression)
.replacingOccurrences(of: "\\*{2,}[/]", with: "*/**/", options: .regularExpression)
.replacingOccurrences(of: "[/]\\*{2,}([^/])", with: "/**/*$1", options: .regularExpression)
.replacingOccurrences(of: "^\\*{2,}([^/])", with: "**/*$1", options: .regularExpression)
for child in input.children {
try self.match(input: child, components: adjustedPattern.split(separator: "/").map(String.init))
}
}
private func match(input: URL, components: [String]) throws {
if components.isEmpty {
var dir = input.standardizedFileURL
while dir != self.input.standardizedFileURL {
results.insert(dir)
guard dir.pathComponents.count > 1 else { break }
dir.deleteLastPathComponent()
}
return input.childrenRecursive.forEach { results.insert($0) }
}
let head = components.first ?? ""
let tail = components.tail
if head == "**" {
var tail: [String] = tail
while tail.first == "**" {
tail = tail.tail
}
try self.match(input: input, components: tail)
for child in input.children {
try self.match(input: child, components: components)
}
return
}
if try glob(input.lastPathComponent, head) {
try self.match(input: input, components: tail)
for child in input.children where try glob(child.lastPathComponent, tail.first ?? "") {
try self.match(input: child, components: tail)
}
return
}
}
func glob(_ input: String, _ pattern: String) throws -> Bool {
let regexPattern =
"^"
+ NSRegularExpression.escapedPattern(for: pattern)
.replacingOccurrences(of: "\\*", with: "[^/]*")
.replacingOccurrences(of: "\\?", with: "[^/]")
.replacingOccurrences(of: "[\\^", with: "[^")
.replacingOccurrences(of: "\\[", with: "[")
.replacingOccurrences(of: "\\]", with: "]") + "$"
// validate the regex pattern created
let _ = try Regex(regexPattern)
return input.range(of: regexPattern, options: .regularExpression) != nil
}
}
extension URL {
var children: [URL] {
(try? FileManager.default.contentsOfDirectory(at: self, includingPropertiesForKeys: nil))
?? []
}
var childrenRecursive: [URL] {
var results: [URL] = []
if let enumerator = FileManager.default.enumerator(
at: self, includingPropertiesForKeys: [.isDirectoryKey, .isSymbolicLinkKey])
{
while let child = enumerator.nextObject() as? URL {
results.append(child)
}
}
return [self] + results
}
}
extension [String] {
var tail: [String] {
if self.count <= 1 {
return []
}
return Array(self.dropFirst())
}
}
@@ -0,0 +1,51 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
struct TerminalCommand: Codable {
let commandType: String
let code: String
let rows: UInt16
let cols: UInt16
enum CodingKeys: String, CodingKey {
case commandType = "command_type"
case code
case rows
case cols
}
init(rows: UInt16, cols: UInt16) {
self.commandType = "terminal"
self.code = "winch"
self.rows = rows
self.cols = cols
}
init() {
self.commandType = "terminal"
self.code = "ack"
self.rows = 0
self.cols = 0
}
func json() throws -> String? {
let encoder = JSONEncoder()
let data = try encoder.encode(self)
return data.base64EncodedString().trimmingCharacters(in: CharacterSet(charactersIn: "="))
}
}
+136
View File
@@ -0,0 +1,136 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
//
import Foundation
extension URL {
func parentOf(_ url: URL) -> Bool {
// if self is a relative path
guard self.cleanPath.hasPrefix("/") else {
return true
}
let pathItems = self.standardizedFileURL.absoluteURL.pathComponents.map { $0.cleanPathComponent }
let urlItems = url.standardizedFileURL.absoluteURL.pathComponents.map { $0.cleanPathComponent }
if pathItems.count > urlItems.count {
return false
}
for (index, pathItem) in pathItems.enumerated() {
if urlItems[index] != pathItem {
return false
}
}
return true
}
func relativeChildPath(to context: URL) throws -> String {
if !context.parentOf(self.absoluteURL.standardizedFileURL) {
throw BuildFSSync.Error.pathIsNotChild(self.cleanPath, context.cleanPath)
}
let pathItems = context.standardizedFileURL.pathComponents.map { $0.cleanPathComponent }
let urlItems = self.standardizedFileURL.pathComponents.map { $0.cleanPathComponent }
return String(urlItems.dropFirst(pathItems.count).joined(separator: "/").trimming { $0 == "/" })
}
var cleanPath: String {
let pathStr = self.path(percentEncoded: false)
if let cleanPath = pathStr.removingPercentEncoding {
return cleanPath
}
return pathStr
}
func relativePathFrom(from base: URL) -> String {
let destComponents = self.standardizedFileURL.pathComponents.map { $0.cleanPathComponent }
let baseComponents = base.standardizedFileURL.pathComponents.map { $0.cleanPathComponent }
// Find the last common path between the two
var lastCommon: Int = 0
while lastCommon < baseComponents.count && lastCommon < destComponents.count && baseComponents[lastCommon] == destComponents[lastCommon] {
lastCommon += 1
}
if lastCommon == 0 {
return self.path
}
var relPath: [String] = []
// Add "../" for each component that's a directory after the common prefix
for i in lastCommon..<baseComponents.count {
let sub = baseComponents[0...i]
let currentPath = URL(filePath: sub.joined(separator: "/"))
let resourceValues: URLResourceValues? = try? currentPath.resourceValues(forKeys: [.isDirectoryKey])
if case let isDirectory = resourceValues?.isDirectory, isDirectory == true {
relPath.append("..")
}
}
relPath.append(contentsOf: destComponents[lastCommon...])
return relPath.joined(separator: "/")
}
func zeroCopyReader(
chunk: Int = 1024 * 1024,
buffer: AsyncStream<Data>.Continuation.BufferingPolicy = .unbounded
) throws -> AsyncStream<Data> {
let path = self.cleanPath
let fd = open(path, O_RDONLY | O_NONBLOCK)
guard fd >= 0 else { throw POSIXError.fromErrno() }
let channel = DispatchIO(
type: .stream,
fileDescriptor: fd,
queue: .global(qos: .userInitiated)
) { errno in
close(fd)
}
channel.setLimit(highWater: chunk)
return AsyncStream(bufferingPolicy: buffer) { continuation in
channel.read(
offset: 0, length: Int.max,
queue: .global(qos: .userInitiated)
) { done, ddata, err in
if err != 0 {
continuation.finish()
return
}
if let ddata, ddata.count > -1 {
let data = Data(ddata)
switch continuation.yield(data) {
case .terminated:
channel.close(flags: .stop)
default: break
}
}
if done {
channel.close(flags: .stop)
continuation.finish()
}
}
}
}
}