Skip to content

Commit d897e9a

Browse files
authored
ContainerRegistry: Provide ContentDescriptors for manifests (#146)
Motivation ---------- In a multi-arch image, several architecture-specific manifests are grouped together under the same [index](https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md). The index has a list of [content descriptors](https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md#properties) pointing to the manifests. Currently, `containertool` creates single-architecture images for which the root object is a manifest. Returning a content descriptor for the manifest allows it to create an index which points to the manifest. On its own this does not allow `containertool` to create multi-arch images, but it opens that possibility for the future. It is a necessary towards being able to write [container images to disk](https://github.com/opencontainers/image-spec/blob/v1.0.1/image-layout.md). Modifications ------------- * Return a ContentDescriptor from getManifest(), in addition to the manifest object * Return a ContentDescriptor from putManifest() Result ------ `containertool` can retrieve a `ContentDescriptor` for a manifest it has written or fetched. Test Plan --------- Existing tests have been updated to match the new signatures and continue to pass.
1 parent 8f9ae3f commit d897e9a

File tree

5 files changed

+50
-64
lines changed

5 files changed

+50
-64
lines changed

Sources/ContainerRegistry/ImageManifest+Digest.swift

Lines changed: 0 additions & 26 deletions
This file was deleted.

Sources/ContainerRegistry/Manifests.swift

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,39 +15,39 @@
1515
public extension RegistryClient {
1616
func putManifest(
1717
repository: ImageReference.Repository,
18-
reference: any ImageReference.Reference,
18+
reference: (any ImageReference.Reference)? = nil,
1919
manifest: ImageManifest
20-
) async throws -> String {
20+
) async throws -> ContentDescriptor {
2121
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
22-
let httpResponse = try await executeRequestThrowing(
23-
// All blob uploads have Content-Type: application/octet-stream on the wire, even if mediatype is different
22+
23+
let encoded = try encoder.encode(manifest)
24+
let digest = digest(of: encoded)
25+
let mediaType = manifest.mediaType ?? "application/vnd.oci.image.manifest.v1+json"
26+
27+
let _ = try await executeRequestThrowing(
2428
.put(
2529
repository,
26-
path: "manifests/\(reference)",
27-
contentType: manifest.mediaType ?? "application/vnd.oci.image.manifest.v1+json"
30+
path: "manifests/\(reference ?? digest)",
31+
contentType: mediaType
2832
),
29-
uploading: manifest,
33+
uploading: encoded,
3034
expectingStatus: .created,
3135
decodingErrors: [.notFound]
3236
)
3337

34-
// The distribution spec says the response MUST contain a Location header
35-
// providing a URL from which the saved manifest can be downloaded.
36-
// However some registries return URLs which cannot be fetched, and
37-
// ECR does not set this header at all.
38-
// If the header is not present, create a suitable value.
39-
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
40-
return httpResponse.response.headerFields[.location]
41-
?? registryURL.distributionEndpoint(forRepository: repository, andEndpoint: "manifests/\(manifest.digest)")
42-
.absoluteString
38+
return ContentDescriptor(
39+
mediaType: mediaType,
40+
digest: "\(digest)",
41+
size: Int64(encoded.count)
42+
)
4343
}
4444

4545
func getManifest(
4646
repository: ImageReference.Repository,
4747
reference: any ImageReference.Reference
48-
) async throws -> ImageManifest {
48+
) async throws -> (ImageManifest, ContentDescriptor) {
4949
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
50-
let (data, _) = try await executeRequestThrowing(
50+
let (data, response) = try await executeRequestThrowing(
5151
.get(
5252
repository,
5353
path: "manifests/\(reference)",
@@ -58,7 +58,14 @@ public extension RegistryClient {
5858
),
5959
decodingErrors: [.notFound]
6060
)
61-
return try decoder.decode(ImageManifest.self, from: data)
61+
return (
62+
try decoder.decode(ImageManifest.self, from: data),
63+
ContentDescriptor(
64+
mediaType: response.headerFields[.contentType] ?? "application/vnd.oci.image.manifest.v1+json",
65+
digest: "\(digest(of: data))",
66+
size: Int64(data.count)
67+
)
68+
)
6269
}
6370

6471
func getIndex(

Sources/containertool/Extensions/RegistryClient+Layers.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ import struct Foundation.Data
1616
import ContainerRegistry
1717

1818
extension RegistryClient {
19-
func getImageManifest(forImage image: ImageReference, architecture: String) async throws -> ImageManifest {
20-
// We pushed the amd64 tag but it points to a single-architecture index, not directly to a manifest
21-
// if we get an index we should get a manifest, otherwise we might get a manifest directly
22-
19+
func getImageManifest(forImage image: ImageReference, architecture: String) async throws -> (
20+
ImageManifest, ContentDescriptor
21+
) {
2322
do {
2423
// Try to retrieve a manifest. If the object with this reference is actually an index, the content-type will not match and
2524
// an error will be thrown.

Sources/containertool/containertool.swift

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -249,12 +249,13 @@ extension RegistryClient {
249249

250250
let baseImageManifest: ImageManifest
251251
let baseImageConfiguration: ImageConfiguration
252+
let baseImageDescriptor: ContentDescriptor
252253
if let source {
253-
baseImageManifest = try await source.getImageManifest(
254+
(baseImageManifest, baseImageDescriptor) = try await source.getImageManifest(
254255
forImage: baseImage,
255256
architecture: architecture
256257
)
257-
log("Found base image manifest: \(baseImageManifest.digest)")
258+
try log("Found base image manifest: \(ImageReference.Digest(baseImageDescriptor.digest))")
258259

259260
baseImageConfiguration = try await source.getImageConfiguration(
260261
forImage: baseImage,
@@ -368,22 +369,25 @@ extension RegistryClient {
368369

369370
// MARK: Upload application manifest
370371

372+
let manifestDescriptor = try await self.putManifest(
373+
repository: destinationImage.repository,
374+
reference: destinationImage.reference,
375+
manifest: manifest
376+
)
377+
378+
if verbose {
379+
log("manifest: \(manifestDescriptor.digest) (\(manifestDescriptor.size) bytes)")
380+
}
381+
371382
// Use the manifest's digest if the user did not provide a human-readable tag
372383
// To support multiarch images, we should also create an an index pointing to
373384
// this manifest.
374385
let reference: ImageReference.Reference
375386
if let tag {
376387
reference = try ImageReference.Tag(tag)
377388
} else {
378-
reference = manifest.digest
389+
reference = try ImageReference.Digest(manifestDescriptor.digest)
379390
}
380-
let location = try await self.putManifest(
381-
repository: destinationImage.repository,
382-
reference: destinationImage.reference,
383-
manifest: manifest
384-
)
385-
386-
if verbose { log(location) }
387391

388392
var result = destinationImage
389393
result.reference = reference

Tests/ContainerRegistryTests/SmokeTests.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,10 @@ struct SmokeTests {
148148
manifest: test_manifest
149149
)
150150

151-
let manifest = try await client.getManifest(repository: repository, reference: ImageReference.Tag("latest"))
151+
let (manifest, _) = try await client.getManifest(
152+
repository: repository,
153+
reference: ImageReference.Tag("latest")
154+
)
152155
#expect(manifest.schemaVersion == 2)
153156
#expect(manifest.config.mediaType == "application/vnd.docker.container.image.v1+json")
154157
#expect(manifest.layers.count == 1)
@@ -181,15 +184,14 @@ struct SmokeTests {
181184
layers: [image_descriptor]
182185
)
183186

184-
let _ = try await client.putManifest(
187+
let descriptor = try await client.putManifest(
185188
repository: repository,
186-
reference: test_manifest.digest,
187189
manifest: test_manifest
188190
)
189191

190-
let manifest = try await client.getManifest(
192+
let (manifest, _) = try await client.getManifest(
191193
repository: repository,
192-
reference: test_manifest.digest
194+
reference: ImageReference.Digest(descriptor.digest)
193195
)
194196
#expect(manifest.schemaVersion == 2)
195197
#expect(manifest.config.mediaType == "application/vnd.docker.container.image.v1+json")

0 commit comments

Comments
 (0)