diff --git a/Sources/ContainerRegistry/ImageDestination.swift b/Sources/ContainerRegistry/ImageDestination.swift index 4e4e88d..81ffce6 100644 --- a/Sources/ContainerRegistry/ImageDestination.swift +++ b/Sources/ContainerRegistry/ImageDestination.swift @@ -82,6 +82,25 @@ public protocol ImageDestination { reference: (any ImageReference.Reference)?, manifest: ImageManifest ) async throws -> ContentDescriptor + + /// Encodes and uploads an image index. + /// + /// - Parameters: + /// - repository: Name of the destination repository. + /// - reference: Optional tag to apply to this index. + /// - index: Index to be uploaded. + /// - Returns: An ContentDescriptor object representing the + /// uploaded index. + /// - Throws: If the index cannot be encoded or the upload fails. + /// + /// An index is a type of manifest. Manifests are not treated as blobs + /// by the distribution specification. They have their own MIME types + /// and are uploaded to different endpoint. + func putIndex( + repository: ImageReference.Repository, + reference: (any ImageReference.Reference)?, + index: ImageIndex + ) async throws -> ContentDescriptor } extension ImageDestination { diff --git a/Sources/ContainerRegistry/RegistryClient+ImageDestination.swift b/Sources/ContainerRegistry/RegistryClient+ImageDestination.swift index bb7f3cf..9c08fd4 100644 --- a/Sources/ContainerRegistry/RegistryClient+ImageDestination.swift +++ b/Sources/ContainerRegistry/RegistryClient+ImageDestination.swift @@ -144,8 +144,8 @@ extension RegistryClient: ImageDestination { /// - 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. + /// uploaded manifest. + /// - Throws: If the manifest 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 @@ -177,4 +177,46 @@ extension RegistryClient: ImageDestination { size: Int64(encoded.count) ) } + + /// Encodes and uploads an image index. + /// + /// - Parameters: + /// - repository: Name of the destination repository. + /// - reference: Optional tag to apply to this index. + /// - index: Index to be uploaded. + /// - Returns: An ContentDescriptor object representing the + /// uploaded index. + /// - Throws: If the index cannot be encoded or the upload fails. + /// + /// An index is a type of manifest. Manifests are not treated as blobs + /// by the distribution specification. They have their own MIME types + /// and are uploaded to different endpoint. + public func putIndex( + repository: ImageReference.Repository, + reference: (any ImageReference.Reference)? = nil, + index: ImageIndex + ) async throws -> ContentDescriptor { + // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests + + let encoded = try encoder.encode(index) + let digest = ImageReference.Digest(of: encoded) + let mediaType = index.mediaType ?? "application/vnd.oci.image.index.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) + ) + } } diff --git a/Sources/containertool/Extensions/RegistryClient+publish.swift b/Sources/containertool/Extensions/RegistryClient+publish.swift index 00d049a..552ee2b 100644 --- a/Sources/containertool/Extensions/RegistryClient+publish.swift +++ b/Sources/containertool/Extensions/RegistryClient+publish.swift @@ -146,14 +146,41 @@ func publishContainerImage( log("manifest: \(manifestDescriptor.digest) (\(manifestDescriptor.size) bytes)") } - // Use the manifest's digest if the user did not provide a human-readable tag + // MARK: Create application index + + let index = ImageIndex( + schemaVersion: 2, + mediaType: "application/vnd.oci.image.index.v1+json", + manifests: [ + ContentDescriptor( + mediaType: manifestDescriptor.mediaType, + digest: manifestDescriptor.digest, + size: Int64(manifestDescriptor.size), + platform: .init(architecture: architecture, os: os) + ) + ] + ) + + // MARK: Upload application manifest + + let indexDescriptor = try await destination.putIndex( + repository: destinationImage.repository, + reference: destinationImage.reference, + index: index + ) + + if verbose { + log("index: \(indexDescriptor.digest) (\(indexDescriptor.size) bytes)") + } + + // Use the index 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 = try ImageReference.Digest(manifestDescriptor.digest) + reference = try ImageReference.Digest(indexDescriptor.digest) } var result = destinationImage diff --git a/scripts/test-containertool-elf-detection.sh b/scripts/test-containertool-elf-detection.sh index 0bfea77..fe3c70d 100755 --- a/scripts/test-containertool-elf-detection.sh +++ b/scripts/test-containertool-elf-detection.sh @@ -45,7 +45,7 @@ FILETYPE=$(file "$PKGPATH/.build/x86_64-swift-linux-musl/debug/hello") log "Executable type: $FILETYPE" IMGREF=$(swift run containertool --repository localhost:5000/elf_test "$PKGPATH/.build/x86_64-swift-linux-musl/debug/hello" --from scratch) -$RUNTIME pull "$IMGREF" +$RUNTIME pull --platform=linux/amd64 "$IMGREF" IMGARCH=$($RUNTIME inspect "$IMGREF" --format "{{.Architecture}}") if [ "$IMGARCH" = "amd64" ] ; then log "x86_64 detection: PASSED" @@ -61,7 +61,7 @@ FILETYPE=$(file "$PKGPATH/.build/x86_64-swift-linux-musl/debug/hello") log "Executable type: $FILETYPE" IMGREF=$(swift run containertool --repository localhost:5000/elf_test "$PKGPATH/.build/aarch64-swift-linux-musl/debug/hello" --from scratch) -$RUNTIME pull "$IMGREF" +$RUNTIME pull --platform=linux/arm64 "$IMGREF" IMGARCH=$($RUNTIME inspect "$IMGREF" --format "{{.Architecture}}") if [ "$IMGARCH" = "arm64" ] ; then log "aarch64 detection: PASSED"