mirror of
https://github.com/apple/container.git
synced 2026-09-09 09:15:42 +00:00
218 lines
8.8 KiB
Swift
218 lines
8.8 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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 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
|
|
/// The description for the image that comprises of its name and a reference to its root descriptor.
|
|
public let description: Description
|
|
|
|
/// A description of the OCI image.
|
|
public struct Description: Sendable {
|
|
/// The string reference of the image.
|
|
public let reference: String
|
|
/// The descriptor identifying the image.
|
|
public let descriptor: Descriptor
|
|
/// The digest for the image.
|
|
public var digest: String { descriptor.digest }
|
|
/// The media type of the image.
|
|
public var mediaType: String { descriptor.mediaType }
|
|
|
|
public init(reference: String, descriptor: Descriptor) {
|
|
self.reference = reference
|
|
self.descriptor = descriptor
|
|
}
|
|
}
|
|
|
|
/// The descriptor for the image.
|
|
public var descriptor: Descriptor { description.descriptor }
|
|
/// The digest of the image.
|
|
public var digest: String { description.digest }
|
|
/// The media type of the image.
|
|
public var mediaType: String { description.mediaType }
|
|
/// The string reference for the image.
|
|
public var reference: String { description.reference }
|
|
|
|
public init(description: Description, contentStore: ContentStore) {
|
|
self.description = description
|
|
self.contentStore = contentStore
|
|
}
|
|
|
|
/// Returns the underlying OCI index for the image.
|
|
public func index() async throws -> Index {
|
|
guard let content: Content = try await contentStore.get(digest: digest) else {
|
|
throw ContainerizationError(.notFound, message: "Content with digest \(digest)")
|
|
}
|
|
return try content.decode()
|
|
}
|
|
|
|
/// Returns the manifest for the specified platform.
|
|
public func manifest(for platform: Platform) async throws -> Manifest {
|
|
let index = try await self.index()
|
|
let desc = index.manifests.first { desc in
|
|
desc.platform == platform
|
|
}
|
|
guard let desc else {
|
|
throw ContainerizationError(.unsupported, message: "Platform \(platform.description)")
|
|
}
|
|
guard let content: Content = try await contentStore.get(digest: desc.digest) else {
|
|
throw ContainerizationError(.notFound, message: "Content with digest \(digest)")
|
|
}
|
|
return try content.decode()
|
|
}
|
|
|
|
/// Returns the descriptor for the given platform. If it does not exist
|
|
/// will throw a ContainerizationError with the code set to .invalidArgument.
|
|
public func descriptor(for platform: Platform) async throws -> Descriptor {
|
|
let index = try await self.index()
|
|
let desc = index.manifests.first { $0.platform == platform }
|
|
guard let desc else {
|
|
throw ContainerizationError(.invalidArgument, message: "unsupported platform \(platform)")
|
|
}
|
|
return desc
|
|
}
|
|
|
|
/// Returns the OCI config for the specified platform.
|
|
public func config(for platform: Platform) async throws -> ContainerizationOCI.Image {
|
|
let manifest = try await self.manifest(for: platform)
|
|
let desc = manifest.config
|
|
guard let content: Content = try await contentStore.get(digest: desc.digest) else {
|
|
throw ContainerizationError(.notFound, message: "Content with digest \(digest)")
|
|
}
|
|
return try content.decode()
|
|
}
|
|
|
|
/// Returns a list of digests to all the referenced OCI objects.
|
|
public func referencedDigests() async throws -> [String] {
|
|
var referenced: [String] = [self.digest.trimmingDigestPrefix]
|
|
let index = try await self.index()
|
|
for manifest in index.manifests {
|
|
referenced.append(manifest.digest.trimmingDigestPrefix)
|
|
guard let m: Manifest = try? await contentStore.get(digest: manifest.digest) else {
|
|
// If the requested digest does not exist or is not a manifest. Skip.
|
|
// Its safe to skip processing this digest as it wont have any child layers.
|
|
continue
|
|
}
|
|
let descs = m.layers + [m.config]
|
|
referenced.append(contentsOf: descs.map { $0.digest.trimmingDigestPrefix })
|
|
}
|
|
return referenced
|
|
}
|
|
|
|
/// Returns a reference to the content blob for the image. The specified digest must be referenced by the image in one of its layers.
|
|
public func getContent(digest: String) async throws -> Content {
|
|
guard try await self.referencedDigests().contains(digest.trimmingDigestPrefix) else {
|
|
throw ContainerizationError(.internalError, message: "Image \(self.reference) does not reference digest \(digest)")
|
|
}
|
|
guard let content: Content = try await contentStore.get(digest: digest) else {
|
|
throw ContainerizationError(.notFound, message: "Content with digest \(digest)")
|
|
}
|
|
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
|