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
26 changes: 0 additions & 26 deletions Sources/ContainerRegistry/ImageManifest+Digest.swift

This file was deleted.

45 changes: 26 additions & 19 deletions Sources/ContainerRegistry/Manifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -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(
Expand Down
7 changes: 3 additions & 4 deletions Sources/containertool/Extensions/RegistryClient+Layers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 14 additions & 10 deletions Sources/containertool/containertool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -368,22 +369,25 @@ 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.
let reference: ImageReference.Reference
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
Expand Down
12 changes: 7 additions & 5 deletions Tests/ContainerRegistryTests/SmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down