Files
container/Sources/NativeBuilder/ContainerBuildExecutor/Executors/ImageOperationExecutor.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

187 lines
7.5 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 ContainerBuildSnapshotter
import Foundation
/// Executes ImageOperation (FROM instructions).
public struct ImageOperationExecutor: OperationExecutor {
public let capabilities: ExecutorCapabilities
public init() {
self.capabilities = ExecutorCapabilities(
supportedOperations: [.image],
maxConcurrency: 3
)
}
public func execute(_ operation: ContainerBuildIR.Operation, context: ExecutionContext) async throws -> ExecutionResult {
guard let imageOp = operation as? ImageOperation else {
throw ExecutorError(
type: .invalidConfiguration,
context: ExecutorError.ErrorContext(
operation: operation, underlyingError: NSError(domain: "Executor", code: 1),
diagnostics: ExecutorError.Diagnostics(environment: [:], workingDirectory: "", recentLogs: [])))
}
let startTime = Date()
do {
// 1. Load/pull the base image and create initial snapshot
let baseSnapshot = try await loadBaseImage(imageOp)
// 2. Prepare and commit the base snapshot directly (no filesystem changes needed here)
let finalSnapshot = try await context.prepareAndCommit(from: baseSnapshot)
// 4. Update context with image configuration
try await updateImageConfiguration(imageOp, context: context)
let duration = Date().timeIntervalSince(startTime)
return ExecutionResult(
snapshot: finalSnapshot,
duration: duration
)
} catch {
throw ExecutorError(
type: .executionFailed,
context: ExecutorError.ErrorContext(
operation: operation,
underlyingError: error,
diagnostics: ExecutorError.Diagnostics(
environment: context.environment.effectiveEnvironment,
workingDirectory: context.workingDirectory,
recentLogs: ["Failed to load base image: \(imageOp.source)", "Error: \(error.localizedDescription)"]
)
)
)
}
}
/// Load a base image and create the initial snapshot.
///
/// This is currently a simulation. When TarSnapshotter is implemented,
/// this will pull actual images and extract their filesystem layers.
///
/// - Parameters:
/// - operation: The image operation
/// - Returns: A base snapshot representing the image filesystem
private func loadBaseImage(_ operation: ImageOperation) async throws -> Snapshot {
// TODO: When TarSnapshotter is implemented, this will:
// 1. Pull the image from registry/load from file (if needed)
// 2. Verify the image (if verification specified)
// 3. Extract the image filesystem layers
// 4. Create a snapshot with the actual image content
// Simulate image loading based on source type
let imageSize: Int64
let imageDigest: Digest
switch operation.source {
case .registry(let reference):
// Simulate pulling from registry
imageSize = 100 * 1024 * 1024 // 100MB
let fakeDataString = "fake-image-\(reference.stringValue)"
guard let fakeData = fakeDataString.data(using: .utf8) else {
throw NSError(domain: "ImageOperationExecutor", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to encode fake image data as UTF-8"])
}
var digestBytes = Data(count: 32)
fakeData.withUnsafeBytes { bytes in
digestBytes.withUnsafeMutableBytes { digestBytesPtr in
if let destBase = digestBytesPtr.baseAddress, let srcBase = bytes.baseAddress {
memcpy(destBase, srcBase, min(32, bytes.count))
}
}
}
imageDigest = try Digest(algorithm: .sha256, bytes: digestBytes)
case .scratch:
// Empty image
imageSize = 0
imageDigest = try Digest(algorithm: .sha256, bytes: Data(count: 32))
case .ociLayout:
// Simulate loading from OCI layout
imageSize = 50 * 1024 * 1024 // 50MB
var digestBytes = Data(count: 32)
digestBytes[0] = 1
digestBytes[1] = 2
digestBytes[2] = 3
imageDigest = try Digest(algorithm: .sha256, bytes: digestBytes)
case .tarball:
// Simulate loading from tarball
imageSize = 75 * 1024 * 1024 // 75MB
var digestBytes = Data(count: 32)
digestBytes[0] = 4
digestBytes[1] = 5
digestBytes[2] = 6
imageDigest = try Digest(algorithm: .sha256, bytes: digestBytes)
}
// Create base snapshot (no parent for base images)
// Provide a concrete mountpoint so snapshotter.prepare can ensure it exists.
let tempMountPoint = FileManager.default.temporaryDirectory
.appendingPathComponent("base-image", isDirectory: true)
.appendingPathComponent(UUID().uuidString, isDirectory: true)
return Snapshot(
digest: imageDigest,
size: imageSize,
parent: nil,
state: .prepared(mountpoint: tempMountPoint)
)
}
/// Update the execution context with image configuration.
///
/// - Parameters:
/// - operation: The image operation
/// - context: The execution context to update
private func updateImageConfiguration(_ operation: ImageOperation, context: ExecutionContext) async throws {
// TODO: When TarSnapshotter is implemented, this will:
// 1. Extract the actual image configuration from the image manifest
// 2. Set environment variables, working directory, user, etc.
// 3. Configure exposed ports, volumes, labels, etc.
// For now, simulate basic image configuration
context.updateImageConfig { config in
// Set basic defaults that most images have
config.env = ["PATH=/usr/local/bin:/usr/bin:/bin"]
config.workingDir = "/"
// Add source-specific configuration
switch operation.source {
case .registry(let reference):
config.labels["source"] = "registry:\(reference.stringValue)"
case .scratch:
config.labels["source"] = "scratch"
case .ociLayout:
config.labels["source"] = "oci-layout"
case .tarball:
config.labels["source"] = "tarball"
}
}
}
public func canExecute(_ operation: ContainerBuildIR.Operation) -> Bool {
operation is ImageOperation
}
}