Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions Sources/ContainerRegistry/ImageDestination.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftContainerPlugin open source project
//
// Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import struct Foundation.Data

/// A destination, such as a registry, to which container images can be uploaded.
public protocol ImageDestination {
/// Checks whether a blob exists.
///
/// - Parameters:
/// - repository: Name of the destination repository.
/// - digest: Digest of the requested blob.
/// - Returns: True if the blob exists, otherwise false.
/// - Throws: If the destination encounters an error.
func blobExists(
repository: ImageReference.Repository,
digest: ImageReference.Digest
) async throws -> Bool

/// Uploads a blob of unstructured data.
///
/// - Parameters:
/// - repository: Name of the destination repository.
/// - mediaType: mediaType field for returned ContentDescriptor.
/// On the wire, all blob uploads are `application/octet-stream'.
/// - data: Object to be uploaded.
/// - Returns: An ContentDescriptor object representing the
/// uploaded blob.
/// - Throws: If the upload fails.
func putBlob(
repository: ImageReference.Repository,
mediaType: String,
data: Data
) async throws -> ContentDescriptor

/// Encodes and uploads a JSON object.
///
/// - Parameters:
/// - repository: Name of the destination repository.
/// - mediaType: mediaType field for returned ContentDescriptor.
/// On the wire, all blob uploads are `application/octet-stream'.
/// - data: Object to be uploaded.
/// - Returns: An ContentDescriptor object representing the
/// uploaded blob.
/// - Throws: If the blob cannot be encoded or the upload fails.
///
/// Some JSON objects, such as ImageConfiguration, are stored
/// in the registry as plain blobs with MIME type "application/octet-stream".
/// This function encodes the data parameter and uploads it as a generic blob.
func putBlob<Body: Encodable>(
repository: ImageReference.Repository,
mediaType: String,
data: Body
) async throws -> ContentDescriptor

/// Encodes and uploads an image manifest.
///
/// - Parameters:
/// - repository: Name of the destination repository.
/// - reference: Optional tag to apply to this manifest.
/// - manifest: Manifest to be uploaded.
/// - Returns: An ContentDescriptor object representing the
/// uploaded blob.
/// - Throws: If the blob cannot be encoded or the upload fails.
///
/// Manifests are not treated as blobs by the distribution specification.
/// They have their own MIME types and are uploaded to different
/// registry endpoints than blobs.
func putManifest(
repository: ImageReference.Repository,
reference: (any ImageReference.Reference)?,
manifest: ImageManifest
) async throws -> ContentDescriptor
}

extension ImageDestination {
/// Uploads a blob of unstructured data.
///
/// - Parameters:
/// - repository: Name of the destination repository.
/// - mediaType: mediaType field for returned ContentDescriptor.
/// On the wire, all blob uploads are `application/octet-stream'.
/// - data: Object to be uploaded.
/// - Returns: An ContentDescriptor object representing the
/// uploaded blob.
/// - Throws: If the upload fails.
public func putBlob(
repository: ImageReference.Repository,
mediaType: String = "application/octet-stream",
data: Data
) async throws -> ContentDescriptor {
try await putBlob(repository: repository, mediaType: mediaType, data: data)
}

/// Upload an image configuration record to the registry.
/// - Parameters:
/// - image: Reference to the image associated with the record.
/// - configuration: An image configuration record
/// - Returns: An `ContentDescriptor` referring to the blob stored in the registry.
/// - Throws: If the blob upload fails.
///
/// Image configuration records are stored as blobs in the registry. This function encodes the provided configuration record and stores it as a blob in the registry.
public func putImageConfiguration(
forImage image: ImageReference,
configuration: ImageConfiguration
) async throws -> ContentDescriptor {
try await putBlob(
repository: image.repository,
mediaType: "application/vnd.oci.image.config.v1+json",
data: configuration
)
}
}
69 changes: 69 additions & 0 deletions Sources/ContainerRegistry/ImageSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftContainerPlugin open source project
//
// Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import struct Foundation.Data

/// A source, such as a registry, from which container images can be fetched.
public protocol ImageSource {
/// Fetches a blob of unstructured data.
///
/// - Parameters:
/// - repository: Name of the source repository.
/// - digest: Digest of the blob.
/// - Returns: The downloaded data.
/// - Throws: If the blob download fails.
func getBlob(
repository: ImageReference.Repository,
digest: ImageReference.Digest
) async throws -> Data

/// Fetches an image manifest.
///
/// - Parameters:
/// - repository: Name of the source repository.
/// - reference: Tag or digest of the manifest to fetch.
/// - Returns: The downloaded manifest.
/// - Throws: If the download fails or the manifest cannot be decoded.
func getManifest(
repository: ImageReference.Repository,
reference: any ImageReference.Reference
) async throws -> (ImageManifest, ContentDescriptor)

/// Fetches an image index.
///
/// - Parameters:
/// - repository: Name of the source repository.
/// - reference: Tag or digest of the index to fetch.
/// - Returns: The downloaded index.
/// - Throws: If the download fails or the index cannot be decoded.
func getIndex(
repository: ImageReference.Repository,
reference: any ImageReference.Reference
) async throws -> ImageIndex

/// Fetches an image configuration from the registry.
///
/// - Parameters:
/// - image: Reference to the image containing the record.
/// - digest: Digest of the configuration object to fetch.
/// - Returns: The image confguration record.
/// - Throws: If the download fails or the configuration record cannot be decoded.
///
/// Image configuration records are stored as blobs in the registry. This function retrieves
/// the requested blob and tries to decode it as a configuration record.
func getImageConfiguration(
forImage image: ImageReference,
digest: ImageReference.Digest
) async throws -> ImageConfiguration
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
//
//===----------------------------------------------------------------------===//

import Foundation
import struct Foundation.URL

public extension RegistryClient {
extension RegistryClient {
/// Checks whether the registry supports v2 of the distribution specification.
/// - Returns: an `true` if the registry supports the distribution specification.
/// - Throws: if the registry does not support the distribution specification.
static func checkAPI(client: HTTPClient, registryURL: URL) async throws -> AuthChallenge {
public static func checkAPI(client: HTTPClient, registryURL: URL) async throws -> AuthChallenge {
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support

// The registry indicates that it supports the v2 protocol by returning a 200 OK response.
Expand Down
50 changes: 0 additions & 50 deletions Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
//
//===----------------------------------------------------------------------===//

import Foundation
import struct Foundation.Data
import struct Foundation.URL
import HTTPTypes

extension RegistryClient {
extension RegistryClient: ImageDestination {
// Internal helper method to initiate a blob upload in 'two shot' mode
func startBlobUploadSession(repository: ImageReference.Repository) async throws -> URL {
// Upload in "two shot" mode.
Expand Down Expand Up @@ -48,10 +49,18 @@ extension RegistryClient {

return locationURL
}
}

public extension RegistryClient {
func blobExists(repository: ImageReference.Repository, digest: ImageReference.Digest) async throws -> Bool {
/// Checks whether a blob exists.
///
/// - Parameters:
/// - repository: Name of the destination repository.
/// - digest: Digest of the requested blob.
/// - Returns: True if the blob exists, otherwise false.
/// - Throws: If the destination encounters an error.
public func blobExists(
repository: ImageReference.Repository,
digest: ImageReference.Digest
) async throws -> Bool {
do {
let _ = try await executeRequestThrowing(
.head(repository, path: "blobs/\(digest)"),
Expand All @@ -61,21 +70,6 @@ public extension RegistryClient {
} catch HTTPClientError.unexpectedStatusCode(status: .notFound, _, _) { return false }
}

/// Fetches an unstructured blob of data from the registry.
///
/// - Parameters:
/// - repository: Name of the repository containing the blob.
/// - digest: Digest of the blob.
/// - Returns: The downloaded data.
/// - Throws: If the blob download fails.
func getBlob(repository: ImageReference.Repository, digest: ImageReference.Digest) async throws -> Data {
try await executeRequestThrowing(
.get(repository, path: "blobs/\(digest)", accepting: ["application/octet-stream"]),
decodingErrors: [.notFound]
)
.data
}

/// Uploads a blob to the registry.
///
/// This function uploads a blob of unstructured data to the registry.
Expand All @@ -87,10 +81,11 @@ public extension RegistryClient {
/// - Returns: An ContentDescriptor object representing the
/// uploaded blob.
/// - Throws: If the blob cannot be encoded or the upload fails.
func putBlob(repository: ImageReference.Repository, mediaType: String = "application/octet-stream", data: Data)
async throws
-> ContentDescriptor
{
public func putBlob(
repository: ImageReference.Repository,
mediaType: String = "application/octet-stream",
data: Data
) async throws -> ContentDescriptor {
// Ask the server to open a session and tell us where to upload our data
let location = try await startBlobUploadSession(repository: repository)

Expand Down Expand Up @@ -133,14 +128,53 @@ public extension RegistryClient {
/// Some JSON objects, such as ImageConfiguration, are stored
/// in the registry as plain blobs with MIME type "application/octet-stream".
/// This function encodes the data parameter and uploads it as a generic blob.
func putBlob<Body: Encodable>(
public func putBlob<Body: Encodable>(
repository: ImageReference.Repository,
mediaType: String = "application/octet-stream",
data: Body
)
async throws -> ContentDescriptor
{
) async throws -> ContentDescriptor {
let encoded = try encoder.encode(data)
return try await putBlob(repository: repository, mediaType: mediaType, data: encoded)
}

/// Encodes and uploads an image manifest.
///
/// - Parameters:
/// - repository: Name of the destination repository.
/// - reference: Optional tag to apply to this manifest.
/// - manifest: Manifest to be uploaded.
/// - Returns: An ContentDescriptor object representing the
/// uploaded blob.
/// - Throws: If the blob cannot be encoded or the upload fails.
///
/// Manifests are not treated as blobs by the distribution specification.
/// They have their own MIME types and are uploaded to different
public func putManifest(
repository: ImageReference.Repository,
reference: (any ImageReference.Reference)? = nil,
manifest: ImageManifest
) async throws -> ContentDescriptor {
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests

let encoded = try encoder.encode(manifest)
let digest = ImageReference.Digest(of: encoded)
let mediaType = manifest.mediaType ?? "application/vnd.oci.image.manifest.v1+json"

let _ = try await executeRequestThrowing(
.put(
repository,
path: "manifests/\(reference ?? digest)",
contentType: mediaType
),
uploading: encoded,
expectingStatus: .created,
decodingErrors: [.notFound]
)

return ContentDescriptor(
mediaType: mediaType,
digest: "\(digest)",
size: Int64(encoded.count)
)
}
}
Loading