Skip to content

Commit 9417b63

Browse files
authored
containertool: Store an index, pointing to the image manifest (#151)
Motivation ---------- A multiarch image consists of an index pointing to one or more architecture-specific manifests. `containertool` currently builds a single architecture image, which can be pushed using just a manifest, but including the index as well is a step towards being able to build multiarch images in future. Modifications ------------- * Add `putIndex` operation to the `ImageDestination` protocol and implement it in `RegistryClient` * Build an index in `publishContainerImage` and print its reference on standard output, instead of the reference of the manifest. Result ------ Although it still builds single-architecture images, a `containertool` image will start with an index pointing to a manifest, instead of a bare manifest. Test Plan --------- All existing tests continue to pass.
1 parent a6966f5 commit 9417b63

File tree

4 files changed

+94
-6
lines changed

4 files changed

+94
-6
lines changed

Sources/ContainerRegistry/ImageDestination.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,25 @@ public protocol ImageDestination {
8282
reference: (any ImageReference.Reference)?,
8383
manifest: ImageManifest
8484
) async throws -> ContentDescriptor
85+
86+
/// Encodes and uploads an image index.
87+
///
88+
/// - Parameters:
89+
/// - repository: Name of the destination repository.
90+
/// - reference: Optional tag to apply to this index.
91+
/// - index: Index to be uploaded.
92+
/// - Returns: An ContentDescriptor object representing the
93+
/// uploaded index.
94+
/// - Throws: If the index cannot be encoded or the upload fails.
95+
///
96+
/// An index is a type of manifest. Manifests are not treated as blobs
97+
/// by the distribution specification. They have their own MIME types
98+
/// and are uploaded to different endpoint.
99+
func putIndex(
100+
repository: ImageReference.Repository,
101+
reference: (any ImageReference.Reference)?,
102+
index: ImageIndex
103+
) async throws -> ContentDescriptor
85104
}
86105

87106
extension ImageDestination {

Sources/ContainerRegistry/RegistryClient+ImageDestination.swift

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ extension RegistryClient: ImageDestination {
144144
/// - reference: Optional tag to apply to this manifest.
145145
/// - manifest: Manifest to be uploaded.
146146
/// - Returns: An ContentDescriptor object representing the
147-
/// uploaded blob.
148-
/// - Throws: If the blob cannot be encoded or the upload fails.
147+
/// uploaded manifest.
148+
/// - Throws: If the manifest cannot be encoded or the upload fails.
149149
///
150150
/// Manifests are not treated as blobs by the distribution specification.
151151
/// They have their own MIME types and are uploaded to different
@@ -177,4 +177,46 @@ extension RegistryClient: ImageDestination {
177177
size: Int64(encoded.count)
178178
)
179179
}
180+
181+
/// Encodes and uploads an image index.
182+
///
183+
/// - Parameters:
184+
/// - repository: Name of the destination repository.
185+
/// - reference: Optional tag to apply to this index.
186+
/// - index: Index to be uploaded.
187+
/// - Returns: An ContentDescriptor object representing the
188+
/// uploaded index.
189+
/// - Throws: If the index cannot be encoded or the upload fails.
190+
///
191+
/// An index is a type of manifest. Manifests are not treated as blobs
192+
/// by the distribution specification. They have their own MIME types
193+
/// and are uploaded to different endpoint.
194+
public func putIndex(
195+
repository: ImageReference.Repository,
196+
reference: (any ImageReference.Reference)? = nil,
197+
index: ImageIndex
198+
) async throws -> ContentDescriptor {
199+
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
200+
201+
let encoded = try encoder.encode(index)
202+
let digest = ImageReference.Digest(of: encoded)
203+
let mediaType = index.mediaType ?? "application/vnd.oci.image.index.v1+json"
204+
205+
let _ = try await executeRequestThrowing(
206+
.put(
207+
repository,
208+
path: "manifests/\(reference ?? digest)",
209+
contentType: mediaType
210+
),
211+
uploading: encoded,
212+
expectingStatus: .created,
213+
decodingErrors: [.notFound]
214+
)
215+
216+
return ContentDescriptor(
217+
mediaType: mediaType,
218+
digest: "\(digest)",
219+
size: Int64(encoded.count)
220+
)
221+
}
180222
}

Sources/containertool/Extensions/RegistryClient+publish.swift

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,41 @@ func publishContainerImage<Source: ImageSource, Destination: ImageDestination>(
146146
log("manifest: \(manifestDescriptor.digest) (\(manifestDescriptor.size) bytes)")
147147
}
148148

149-
// Use the manifest's digest if the user did not provide a human-readable tag
149+
// MARK: Create application index
150+
151+
let index = ImageIndex(
152+
schemaVersion: 2,
153+
mediaType: "application/vnd.oci.image.index.v1+json",
154+
manifests: [
155+
ContentDescriptor(
156+
mediaType: manifestDescriptor.mediaType,
157+
digest: manifestDescriptor.digest,
158+
size: Int64(manifestDescriptor.size),
159+
platform: .init(architecture: architecture, os: os)
160+
)
161+
]
162+
)
163+
164+
// MARK: Upload application manifest
165+
166+
let indexDescriptor = try await destination.putIndex(
167+
repository: destinationImage.repository,
168+
reference: destinationImage.reference,
169+
index: index
170+
)
171+
172+
if verbose {
173+
log("index: \(indexDescriptor.digest) (\(indexDescriptor.size) bytes)")
174+
}
175+
176+
// Use the index digest if the user did not provide a human-readable tag
150177
// To support multiarch images, we should also create an an index pointing to
151178
// this manifest.
152179
let reference: ImageReference.Reference
153180
if let tag {
154181
reference = try ImageReference.Tag(tag)
155182
} else {
156-
reference = try ImageReference.Digest(manifestDescriptor.digest)
183+
reference = try ImageReference.Digest(indexDescriptor.digest)
157184
}
158185

159186
var result = destinationImage

scripts/test-containertool-elf-detection.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ FILETYPE=$(file "$PKGPATH/.build/x86_64-swift-linux-musl/debug/hello")
4545
log "Executable type: $FILETYPE"
4646

4747
IMGREF=$(swift run containertool --repository localhost:5000/elf_test "$PKGPATH/.build/x86_64-swift-linux-musl/debug/hello" --from scratch)
48-
$RUNTIME pull "$IMGREF"
48+
$RUNTIME pull --platform=linux/amd64 "$IMGREF"
4949
IMGARCH=$($RUNTIME inspect "$IMGREF" --format "{{.Architecture}}")
5050
if [ "$IMGARCH" = "amd64" ] ; then
5151
log "x86_64 detection: PASSED"
@@ -61,7 +61,7 @@ FILETYPE=$(file "$PKGPATH/.build/x86_64-swift-linux-musl/debug/hello")
6161
log "Executable type: $FILETYPE"
6262

6363
IMGREF=$(swift run containertool --repository localhost:5000/elf_test "$PKGPATH/.build/aarch64-swift-linux-musl/debug/hello" --from scratch)
64-
$RUNTIME pull "$IMGREF"
64+
$RUNTIME pull --platform=linux/arm64 "$IMGREF"
6565
IMGARCH=$($RUNTIME inspect "$IMGREF" --format "{{.Architecture}}")
6666
if [ "$IMGARCH" = "arm64" ] ; then
6767
log "aarch64 detection: PASSED"

0 commit comments

Comments
 (0)