mirror of
https://github.com/apple/container.git
synced 2026-09-22 15:45:38 +00:00
Create unpacker protocol + ext4 unpacker (#151)
Creates a new Unpacker protocol that defines a single method ``` func unpack(_ image: Image, for platform: Platform, at path: URL, progress: ProgressHandler?) async throws -> Mount ``` This change also removes the `unpack(...)` method from the Image type. Before ``` let mount = try await image.unpack(for: platform, at: path) ``` After ``` let unpacker = EXT4Unpacker(blockSizeInBytes: 2.gib()) let mount = try await unpacker.unpack(image, for: platform, at: path) ``` --------- Signed-off-by: Aditya Ramani <a_ramani@apple.com>
This commit is contained in:
@@ -19,13 +19,6 @@ import ContainerizationOCI
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
|
||||
#if os(macOS)
|
||||
import ContainerizationArchive
|
||||
import ContainerizationEXT4
|
||||
import SystemPackage
|
||||
import ContainerizationExtras
|
||||
#endif
|
||||
|
||||
/// Type representing an OCI container image.
|
||||
public struct Image: Sendable {
|
||||
private let contentStore: ContentStore
|
||||
@@ -135,83 +128,3 @@ public struct Image: Sendable {
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
extension Image {
|
||||
/// Unpack the image into a filesystem.
|
||||
public func unpack(for platform: Platform, at path: URL, blockSizeInBytes: UInt64 = 512.gib(), progress: ProgressHandler? = nil) async throws -> Mount {
|
||||
let blockPath = try prepareUnpackPath(path: path)
|
||||
let manifest = try await loadManifest(platform: platform)
|
||||
return try await unpackContents(
|
||||
path: blockPath,
|
||||
manifest: manifest,
|
||||
blockSizeInBytes: blockSizeInBytes,
|
||||
progress: progress
|
||||
)
|
||||
}
|
||||
|
||||
private func loadManifest(platform: Platform) async throws -> Manifest {
|
||||
let manifest = try await descriptor(for: platform)
|
||||
guard let m: Manifest = try await self.contentStore.get(digest: manifest.digest) else {
|
||||
throw ContainerizationError(.notFound, message: "content not found \(manifest.digest)")
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
private func prepareUnpackPath(path: URL) throws -> String {
|
||||
let blockPath = path.absolutePath()
|
||||
guard !FileManager.default.fileExists(atPath: blockPath) else {
|
||||
throw ContainerizationError(.exists, message: "block device already exists at \(blockPath)")
|
||||
}
|
||||
return blockPath
|
||||
}
|
||||
|
||||
private func unpackContents(path: String, manifest: Manifest, blockSizeInBytes: UInt64, progress: ProgressHandler?) async throws -> Mount {
|
||||
let filesystem = try EXT4.Formatter(FilePath(path), minDiskSize: blockSizeInBytes)
|
||||
defer { try? filesystem.close() }
|
||||
|
||||
for layer in manifest.layers {
|
||||
try Task.checkCancellation()
|
||||
guard let content = try await self.contentStore.get(digest: layer.digest) else {
|
||||
throw ContainerizationError(.notFound, message: "Content with digest \(layer.digest)")
|
||||
}
|
||||
|
||||
switch layer.mediaType {
|
||||
case MediaTypes.imageLayer, MediaTypes.dockerImageLayer:
|
||||
try filesystem.unpack(
|
||||
source: content.path,
|
||||
format: .paxRestricted,
|
||||
compression: .none,
|
||||
progress: progress
|
||||
)
|
||||
case MediaTypes.imageLayerGzip, MediaTypes.dockerImageLayerGzip:
|
||||
try filesystem.unpack(
|
||||
source: content.path,
|
||||
format: .paxRestricted,
|
||||
compression: .gzip,
|
||||
progress: progress
|
||||
)
|
||||
default:
|
||||
throw ContainerizationError(.unsupported, message: "Media type \(layer.mediaType) not supported.")
|
||||
}
|
||||
}
|
||||
|
||||
return .block(
|
||||
format: "ext4",
|
||||
source: path,
|
||||
destination: "/",
|
||||
options: []
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
extension Image {
|
||||
public func unpack(for platform: Platform, at path: URL, blockSizeInBytes: UInt64 = 512.gib()) async throws -> Mount {
|
||||
throw ContainerizationError(.unsupported, message: "Image unpack unsupported on current platform")
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -34,7 +34,8 @@ public struct InitImage: Sendable {
|
||||
extension InitImage {
|
||||
/// Unpack the initial filesystem for the desired platform at a given path.
|
||||
public func initBlock(at: URL, for platform: SystemPlatform) async throws -> Mount {
|
||||
var fs = try await image.unpack(for: platform.ociPlatform(), at: at, blockSizeInBytes: 512.mib())
|
||||
let unpacker = EXT4Unpacker(blockSizeInBytes: 512.mib())
|
||||
var fs = try await unpacker.unpack(self.image, for: platform.ociPlatform(), at: at)
|
||||
fs.options = ["ro"]
|
||||
return fs
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization 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 ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import ContainerizationOCI
|
||||
import Foundation
|
||||
|
||||
#if os(macOS)
|
||||
import ContainerizationArchive
|
||||
import ContainerizationEXT4
|
||||
import SystemPackage
|
||||
#endif
|
||||
|
||||
public struct EXT4Unpacker: Unpacker {
|
||||
let blockSizeInBytes: UInt64
|
||||
|
||||
public init(blockSizeInBytes: UInt64) {
|
||||
self.blockSizeInBytes = blockSizeInBytes
|
||||
}
|
||||
|
||||
public func unpack(_ image: Image, for platform: Platform, at path: URL, progress: ProgressHandler? = nil) async throws -> Mount {
|
||||
#if !os(macOS)
|
||||
throw ContainerizationError(.unsupported, message: "Cannot unpack an image on current platform")
|
||||
#else
|
||||
let blockPath = try prepareUnpackPath(path: path)
|
||||
let manifest = try await image.manifest(for: platform)
|
||||
let filesystem = try EXT4.Formatter(FilePath(path), minDiskSize: blockSizeInBytes)
|
||||
defer { try? filesystem.close() }
|
||||
|
||||
for layer in manifest.layers {
|
||||
try Task.checkCancellation()
|
||||
let content = try await image.getContent(digest: layer.digest)
|
||||
|
||||
let compression: ContainerizationArchive.Filter
|
||||
switch layer.mediaType {
|
||||
case MediaTypes.imageLayer, MediaTypes.dockerImageLayer:
|
||||
compression = .none
|
||||
case MediaTypes.imageLayerGzip, MediaTypes.dockerImageLayerGzip:
|
||||
compression = .gzip
|
||||
default:
|
||||
throw ContainerizationError(.unsupported, message: "Media type \(layer.mediaType) not supported.")
|
||||
}
|
||||
try filesystem.unpack(
|
||||
source: content.path,
|
||||
format: .paxRestricted,
|
||||
compression: compression,
|
||||
progress: progress
|
||||
)
|
||||
}
|
||||
|
||||
return .block(
|
||||
format: "ext4",
|
||||
source: blockPath,
|
||||
destination: "/",
|
||||
options: []
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func prepareUnpackPath(path: URL) throws -> String {
|
||||
let blockPath = path.absolutePath()
|
||||
guard !FileManager.default.fileExists(atPath: blockPath) else {
|
||||
throw ContainerizationError(.exists, message: "block device already exists at \(blockPath)")
|
||||
}
|
||||
return blockPath
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
import ContainerizationOCI
|
||||
import Foundation
|
||||
|
||||
/// The `Unpacker` protocol defines a standardized interface that involves
|
||||
/// decompressing, extracting image layers and preparing it for use.
|
||||
///
|
||||
/// The `Unpacker` is responsible for managing the lifecycle of the
|
||||
/// unpacking process, including any temporary files or resources, until the
|
||||
/// `Mount` object is produced.
|
||||
public protocol Unpacker {
|
||||
|
||||
/// Unpacks the provided image to a specified path for a given platform.
|
||||
///
|
||||
/// This asynchronous method should handle the entire unpacking process, from reading
|
||||
/// the `Image` layers for the given `Platform` via its `Manifest`,
|
||||
/// to making the extracted contents available as a `Mount`.
|
||||
/// Implementations of this method may apply platform-specific optimizations
|
||||
/// or transformations during the unpacking.
|
||||
///
|
||||
/// Progress updates can be observed via the optional `progress` handler.
|
||||
func unpack(_ image: Image, for platform: Platform, at path: URL, progress: ProgressHandler?) async throws -> Mount
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user