diff --git a/Package.swift b/Package.swift index 4d070ff0..8cbe16ac 100644 --- a/Package.swift +++ b/Package.swift @@ -90,7 +90,10 @@ let package = Package( name: "ContainerizationUnitTests", dependencies: ["Containerization"], path: "Tests/ContainerizationTests", - resources: [.copy("ImageTests/Resources/scratch.tar")] + resources: [ + .copy("ImageTests/Resources/scratch.tar"), + .copy("ImageTests/Resources/scratch_no_annotations.tar"), + ] ), .target( name: "ContainerizationEXT4", diff --git a/Sources/Containerization/Image/ImageStore/ImageStore+OCILayout.swift b/Sources/Containerization/Image/ImageStore/ImageStore+OCILayout.swift index b51d0b00..cf89b121 100644 --- a/Sources/Containerization/Image/ImageStore/ImageStore+OCILayout.swift +++ b/Sources/Containerization/Image/ImageStore/ImageStore+OCILayout.swift @@ -80,9 +80,7 @@ extension ImageStore { let (id, tempDir) = try await self.contentStore.newIngestSession() do { for descriptor in index.manifests { - guard let reference = client.getImageReferencefromDescriptor(descriptor: descriptor) else { - continue - } + let reference = client.getImageReferencefromDescriptor(descriptor: descriptor) let ref = try Reference.parse(reference) let name = ref.path let operation = ImportOperation(name: name, contentStore: self.contentStore, client: client, ingestDir: tempDir, progress: progress) diff --git a/Sources/ContainerizationOCI/Client/LocalOCILayoutClient.swift b/Sources/ContainerizationOCI/Client/LocalOCILayoutClient.swift index 3e25b53e..299ec60e 100644 --- a/Sources/ContainerizationOCI/Client/LocalOCILayoutClient.swift +++ b/Sources/ContainerizationOCI/Client/LocalOCILayoutClient.swift @@ -205,11 +205,8 @@ extension LocalOCILayoutClient { descriptor.annotations = annotations } - package func getImageReferencefromDescriptor(descriptor: Descriptor) -> String? { + package func getImageReferencefromDescriptor(descriptor: Descriptor) -> String { let annotations = descriptor.annotations - guard let annotations else { - return nil - } // Annotations here do not conform to the OCI image specification. // The interpretation of the annotations "org.opencontainers.image.ref.name" and @@ -220,16 +217,21 @@ extension LocalOCILayoutClient { // https://github.com/moby/buildkit/issues/4615#issuecomment-2521810830 // Until a consensus is reached, the preference is given to "com.apple.containerization.image.name" and then to // using "io.containerd.image.name" as it is the next safest choice - if let name = annotations[AnnotationKeys.containerizationImageName] { - return name + if let annotations { + if let name = annotations[AnnotationKeys.containerizationImageName] { + return name + } + if let name = annotations[AnnotationKeys.containerdImageName] { + return name + } + if let name = annotations[AnnotationKeys.openContainersImageName] { + return name + } } - if let name = annotations[AnnotationKeys.containerdImageName] { - return name - } - if let name = annotations[AnnotationKeys.openContainersImageName] { - return name - } - return nil + + // Fallback: Generate digest-based reference for images without annotations + // This makes sure OCI spec compliance as annotations are optional + return "untagged@\(descriptor.digest)" } package enum Error: Swift.Error { diff --git a/Tests/ContainerizationTests/ImageTests/ImageStoreTests.swift b/Tests/ContainerizationTests/ImageTests/ImageStoreTests.swift index daab699b..622d1be8 100644 --- a/Tests/ContainerizationTests/ImageTests/ImageStoreTests.swift +++ b/Tests/ContainerizationTests/ImageTests/ImageStoreTests.swift @@ -87,4 +87,26 @@ public class ImageStoreTests: ContainsAuth { let _ = try await self.store.tag(existing: imageReference, new: upstreamTag) try await self.store.push(reference: upstreamTag, auth: authentication) } + + @Test func testLoadImageWithoutAnnotations() async throws { + let fileManager = FileManager.default + let tempDir = fileManager.uniqueTemporaryDirectory() + defer { + try? fileManager.removeItem(at: tempDir) + } + + let tarPath = Foundation.Bundle.module.url(forResource: "scratch_no_annotations", withExtension: "tar")! + let reader = try ArchiveReader(format: .pax, filter: .none, file: tarPath) + try reader.extractContents(to: tempDir) + + let loaded = try await self.store.load(from: tempDir) + + #expect(loaded.count == 1) + + let reference = loaded.first!.reference + #expect(reference.hasPrefix("untagged@sha256:")) + + let retrieved = try await self.store.get(reference: reference) + #expect(retrieved.reference == reference) + } } diff --git a/Tests/ContainerizationTests/ImageTests/Resources/scratch_no_annotations.tar b/Tests/ContainerizationTests/ImageTests/Resources/scratch_no_annotations.tar new file mode 100644 index 00000000..37135a10 Binary files /dev/null and b/Tests/ContainerizationTests/ImageTests/Resources/scratch_no_annotations.tar differ