diff --git a/Sources/ContainerRegistry/ImageManifest+Digest.swift b/Sources/ContainerRegistry/ImageManifest+Digest.swift deleted file mode 100644 index 2272979..0000000 --- a/Sources/ContainerRegistry/ImageManifest+Digest.swift +++ /dev/null @@ -1,26 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftContainerPlugin open source project -// -// Copyright (c) 2024 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 Foundation -import struct Crypto.SHA256 - -public extension ImageManifest { - var digest: ImageReference.Digest { - let encoder = JSONEncoder() - encoder.outputFormatting = [.sortedKeys, .prettyPrinted, .withoutEscapingSlashes] - encoder.dateEncodingStrategy = .iso8601 - let encoded = try! encoder.encode(self) - return ContainerRegistry.digest(of: encoded) - } -} diff --git a/Sources/ContainerRegistry/Manifests.swift b/Sources/ContainerRegistry/Manifests.swift index b1681d8..cda4794 100644 --- a/Sources/ContainerRegistry/Manifests.swift +++ b/Sources/ContainerRegistry/Manifests.swift @@ -15,39 +15,39 @@ public extension RegistryClient { func putManifest( repository: ImageReference.Repository, - reference: any ImageReference.Reference, + reference: (any ImageReference.Reference)? = nil, manifest: ImageManifest - ) async throws -> String { + ) async throws -> ContentDescriptor { // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests - let httpResponse = try await executeRequestThrowing( - // All blob uploads have Content-Type: application/octet-stream on the wire, even if mediatype is different + + let encoded = try encoder.encode(manifest) + let digest = digest(of: encoded) + let mediaType = manifest.mediaType ?? "application/vnd.oci.image.manifest.v1+json" + + let _ = try await executeRequestThrowing( .put( repository, - path: "manifests/\(reference)", - contentType: manifest.mediaType ?? "application/vnd.oci.image.manifest.v1+json" + path: "manifests/\(reference ?? digest)", + contentType: mediaType ), - uploading: manifest, + uploading: encoded, expectingStatus: .created, decodingErrors: [.notFound] ) - // The distribution spec says the response MUST contain a Location header - // providing a URL from which the saved manifest can be downloaded. - // However some registries return URLs which cannot be fetched, and - // ECR does not set this header at all. - // If the header is not present, create a suitable value. - // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests - return httpResponse.response.headerFields[.location] - ?? registryURL.distributionEndpoint(forRepository: repository, andEndpoint: "manifests/\(manifest.digest)") - .absoluteString + return ContentDescriptor( + mediaType: mediaType, + digest: "\(digest)", + size: Int64(encoded.count) + ) } func getManifest( repository: ImageReference.Repository, reference: any ImageReference.Reference - ) async throws -> ImageManifest { + ) async throws -> (ImageManifest, ContentDescriptor) { // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests - let (data, _) = try await executeRequestThrowing( + let (data, response) = try await executeRequestThrowing( .get( repository, path: "manifests/\(reference)", @@ -58,7 +58,14 @@ public extension RegistryClient { ), decodingErrors: [.notFound] ) - return try decoder.decode(ImageManifest.self, from: data) + return ( + try decoder.decode(ImageManifest.self, from: data), + ContentDescriptor( + mediaType: response.headerFields[.contentType] ?? "application/vnd.oci.image.manifest.v1+json", + digest: "\(digest(of: data))", + size: Int64(data.count) + ) + ) } func getIndex( diff --git a/Sources/containertool/Extensions/RegistryClient+Layers.swift b/Sources/containertool/Extensions/RegistryClient+Layers.swift index 79ffc6c..87ac73f 100644 --- a/Sources/containertool/Extensions/RegistryClient+Layers.swift +++ b/Sources/containertool/Extensions/RegistryClient+Layers.swift @@ -16,10 +16,9 @@ import struct Foundation.Data import ContainerRegistry extension RegistryClient { - func getImageManifest(forImage image: ImageReference, architecture: String) async throws -> ImageManifest { - // We pushed the amd64 tag but it points to a single-architecture index, not directly to a manifest - // if we get an index we should get a manifest, otherwise we might get a manifest directly - + func getImageManifest(forImage image: ImageReference, architecture: String) async throws -> ( + ImageManifest, ContentDescriptor + ) { do { // Try to retrieve a manifest. If the object with this reference is actually an index, the content-type will not match and // an error will be thrown. diff --git a/Sources/containertool/containertool.swift b/Sources/containertool/containertool.swift index 4621868..28ee2d4 100644 --- a/Sources/containertool/containertool.swift +++ b/Sources/containertool/containertool.swift @@ -249,12 +249,13 @@ extension RegistryClient { let baseImageManifest: ImageManifest let baseImageConfiguration: ImageConfiguration + let baseImageDescriptor: ContentDescriptor if let source { - baseImageManifest = try await source.getImageManifest( + (baseImageManifest, baseImageDescriptor) = try await source.getImageManifest( forImage: baseImage, architecture: architecture ) - log("Found base image manifest: \(baseImageManifest.digest)") + try log("Found base image manifest: \(ImageReference.Digest(baseImageDescriptor.digest))") baseImageConfiguration = try await source.getImageConfiguration( forImage: baseImage, @@ -368,6 +369,16 @@ extension RegistryClient { // MARK: Upload application manifest + let manifestDescriptor = try await self.putManifest( + repository: destinationImage.repository, + reference: destinationImage.reference, + manifest: manifest + ) + + if verbose { + log("manifest: \(manifestDescriptor.digest) (\(manifestDescriptor.size) bytes)") + } + // Use the manifest's digest if the user did not provide a human-readable tag // To support multiarch images, we should also create an an index pointing to // this manifest. @@ -375,15 +386,8 @@ extension RegistryClient { if let tag { reference = try ImageReference.Tag(tag) } else { - reference = manifest.digest + reference = try ImageReference.Digest(manifestDescriptor.digest) } - let location = try await self.putManifest( - repository: destinationImage.repository, - reference: destinationImage.reference, - manifest: manifest - ) - - if verbose { log(location) } var result = destinationImage result.reference = reference diff --git a/Tests/ContainerRegistryTests/SmokeTests.swift b/Tests/ContainerRegistryTests/SmokeTests.swift index af240dc..30f07b5 100644 --- a/Tests/ContainerRegistryTests/SmokeTests.swift +++ b/Tests/ContainerRegistryTests/SmokeTests.swift @@ -148,7 +148,10 @@ struct SmokeTests { manifest: test_manifest ) - let manifest = try await client.getManifest(repository: repository, reference: ImageReference.Tag("latest")) + let (manifest, _) = try await client.getManifest( + repository: repository, + reference: ImageReference.Tag("latest") + ) #expect(manifest.schemaVersion == 2) #expect(manifest.config.mediaType == "application/vnd.docker.container.image.v1+json") #expect(manifest.layers.count == 1) @@ -181,15 +184,14 @@ struct SmokeTests { layers: [image_descriptor] ) - let _ = try await client.putManifest( + let descriptor = try await client.putManifest( repository: repository, - reference: test_manifest.digest, manifest: test_manifest ) - let manifest = try await client.getManifest( + let (manifest, _) = try await client.getManifest( repository: repository, - reference: test_manifest.digest + reference: ImageReference.Digest(descriptor.digest) ) #expect(manifest.schemaVersion == 2) #expect(manifest.config.mediaType == "application/vnd.docker.container.image.v1+json")