Files
container/Sources/NativeBuilder/ContainerBuildExecutor/ReportingHelpers.swift
T
Sidhartha Mani d2f48982c1 Native Builder: Define Snapshotter protocol (#491)
This PR defines the snapshotter protocol

```swift
    ///Mount a snapshot and all its previous layers
    func prepare(_ snapshot: Snapshot) async throws -> Snapshot

    /// Commit a snapshot, making it permanent.
    func commit(_ snapshot: Snapshot) async throws -> Snapshot

    /// Remove a snapshot from snapshot store
    func remove(_ snapshot: Snapshot) async throws
```

It updates executors to work with this new protocol
2025-08-12 23:30:56 -07:00

205 lines
7.6 KiB
Swift

//===----------------------------------------------------------------------===//
// 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 ContainerBuildIR
import ContainerBuildReporting
import Foundation
// MARK: - Helper Extensions
extension ReportContext {
/// Create context from a build node and stage
public init(node: BuildNode, stage: BuildStage, operation: any ContainerBuildIR.Operation) {
self.init(
nodeId: node.id,
stageId: stage.name ?? "stage-\(stage.id.uuidString.prefix(8))",
description: Self.describeOperation(operation),
timestamp: Date(),
sourceMap: nil
)
}
/// Generate a human-readable description for an operation
public static func describeOperation(_ operation: any ContainerBuildIR.Operation) -> String {
switch operation {
case let exec as ExecOperation:
return "RUN \(exec.command.displayString)"
case let fs as FilesystemOperation:
switch fs.action {
case .copy:
return "COPY \(Self.describeSource(fs.source)) \(fs.destination)"
case .add:
return "ADD \(Self.describeSource(fs.source)) \(fs.destination)"
case .remove:
return "REMOVE \(fs.destination)"
case .mkdir:
return "MKDIR \(fs.destination)"
case .symlink:
return "SYMLINK \(fs.destination)"
case .hardlink:
return "HARDLINK \(fs.destination)"
}
case let img as ImageOperation:
switch img.source {
case .registry(let ref):
return "FROM \(ref.stringValue)"
case .scratch:
return "FROM scratch"
case .ociLayout(let path, let tag):
return "FROM oci-layout:\(path)\(tag.map { ":\($0)" } ?? "")"
case .tarball(let path):
return "FROM tarball:\(path)"
}
case let meta as MetadataOperation:
switch meta.action {
case .setEnv(let key, let value):
return "ENV \(key)=\(Self.describeEnvValue(value))"
case .setEnvBatch(let vars):
return "ENV \(vars.map { "\($0.key)=\(Self.describeEnvValue($0.value))" }.joined(separator: " "))"
case .setWorkdir(let path):
return "WORKDIR \(path)"
case .setUser(let user):
return "USER \(Self.describeUser(user))"
case .setEntrypoint(let cmd):
return "ENTRYPOINT \(cmd.displayString)"
case .setCmd(let cmd):
return "CMD \(cmd.displayString)"
case .setLabel(let key, let value):
return "LABEL \(key)=\(value)"
case .setLabelBatch(let labels):
return "LABEL \(labels.map { "\($0.key)=\($0.value)" }.joined(separator: " "))"
case .declareArg(let name, let defaultValue):
return "ARG \(name)\(defaultValue.map { "=\($0)" } ?? "")"
case .expose(let ports):
return "EXPOSE \(ports.map{$0.stringValue}.joined(separator: " "))"
case .setStopSignal(let signal):
return "STOPSIGNAL \(signal)"
case .setHealthcheck(let hc):
guard let hc = hc else {
return "HEALTHCHECK NONE"
}
switch hc.test {
case .none:
return "HEALTHCHECK NONE"
case .command(let cmd):
return "HEALTHCHECK CMD \(cmd.displayString)"
case .shell(let cmd):
return "HEALTHCHECK CMD-SHELL \(cmd)"
}
case .setShell(let shell):
return "SHELL [\(shell.map { "\"\($0)\"" }.joined(separator: ", "))]"
case .addVolume(let path):
return "VOLUME \(path)"
case .addOnBuild(let instruction):
return "ONBUILD \(instruction)"
}
default:
return "Operation \(operation.operationKind.rawValue)"
}
}
private static func describeSource(_ source: FilesystemSource) -> String {
switch source {
case .context(let ctx):
return "\(ctx.name):\(ctx.paths.joined(separator: " "))"
case .stage(let ref, let paths):
let prefix: String
switch ref {
case .named(let name):
prefix = name
case .index(let idx):
prefix = "stage-\(idx)"
case .previous:
prefix = "previous"
}
return "\(prefix):\(paths.joined(separator: " "))"
case .image(let ref, let paths):
return "\(ref.stringValue):\(paths.joined(separator: " "))"
case .url(let url):
return url.absoluteString
case .git(let src):
return src.repository
case .inline(_):
return "<inline>"
case .scratch:
return "scratch"
}
}
private static func describeEnvValue(_ value: EnvironmentValue) -> String {
switch value {
case .literal(let str):
return str
case .buildArg(let name):
return "${\(name)}"
case .expansion(let name, let defaultValue):
guard let defaultValue = defaultValue else {
return "${\(name)}"
}
return "${\(name):-\(defaultValue)}"
}
}
private static func describeUser(_ user: User) -> String {
switch user {
case .named(let name):
return name
case .uid(let uid):
return String(uid)
case .userGroup(let user, let group):
return "\(user):\(group)"
case .uidGid(let uid, let gid):
return "\(uid):\(gid)"
}
}
}
extension BuildEventError {
/// Create from ExecutorError
public init(from executorError: ExecutorError) {
let failureType: FailureType
switch executorError.type {
case .executionFailed:
failureType = .executionFailed
case .cancelled:
failureType = .cancelled
case .invalidConfiguration:
failureType = .invalidConfiguration
case .unsupportedOperation:
failureType = .executionFailed // Map unsupported operations to execution failures
}
var diags: [String: String] = [:]
diags["workingDirectory"] = executorError.context.diagnostics.workingDirectory
for (key, value) in executorError.context.diagnostics.environment {
diags["env.\(key)"] = value
}
if !executorError.context.diagnostics.recentLogs.isEmpty {
diags["recentLogs"] = executorError.context.diagnostics.recentLogs.joined(separator: "\n")
}
self.init(
type: failureType,
description: executorError.context.underlyingError.localizedDescription,
diagnostics: diags
)
}
}