diff --git a/Sources/ContainerRegistry/Blobs.swift b/Sources/ContainerRegistry/Blobs.swift index ea9edaf..96708ee 100644 --- a/Sources/ContainerRegistry/Blobs.swift +++ b/Sources/ContainerRegistry/Blobs.swift @@ -65,9 +65,7 @@ extension RegistryClient { extension HTTPField.Name { static let dockerContentDigest = Self("Docker-Content-Digest")! } public extension RegistryClient { - func blobExists(repository: ImageReference.Repository, digest: String) async throws -> Bool { - precondition(digest.count > 0) - + func blobExists(repository: ImageReference.Repository, digest: ImageReference.Digest) async throws -> Bool { do { let _ = try await executeRequestThrowing( .head(repository, path: "blobs/\(digest)"), @@ -84,10 +82,8 @@ public extension RegistryClient { /// - digest: Digest of the blob. /// - Returns: The downloaded data. /// - Throws: If the blob download fails. - func getBlob(repository: ImageReference.Repository, digest: String) async throws -> Data { - precondition(digest.count > 0, "digest must not be an empty string") - - return try await executeRequestThrowing( + func getBlob(repository: ImageReference.Repository, digest: ImageReference.Digest) async throws -> Data { + try await executeRequestThrowing( .get(repository, path: "blobs/\(digest)", accepting: ["application/octet-stream"]), decodingErrors: [.notFound] ) @@ -106,10 +102,10 @@ public extension RegistryClient { /// in the registry as plain blobs with MIME type "application/octet-stream". /// This function attempts to decode the received data without reference /// to the MIME type. - func getBlob(repository: ImageReference.Repository, digest: String) async throws -> Response { - precondition(digest.count > 0, "digest must not be an empty string") - - return try await executeRequestThrowing( + func getBlob(repository: ImageReference.Repository, digest: ImageReference.Digest) async throws + -> Response + { + try await executeRequestThrowing( .get(repository, path: "blobs/\(digest)", accepting: ["application/octet-stream"]), decodingErrors: [.notFound] ) diff --git a/Sources/ContainerRegistry/ImageReference.swift b/Sources/ContainerRegistry/ImageReference.swift index 5660635..f466de1 100644 --- a/Sources/ContainerRegistry/ImageReference.swift +++ b/Sources/ContainerRegistry/ImageReference.swift @@ -12,8 +12,6 @@ // //===----------------------------------------------------------------------===// -import RegexBuilder - // https://github.com/distribution/distribution/blob/v2.7.1/reference/reference.go // Split the image reference into a registry and a name part. func splitReference(_ reference: String) throws -> (String?, String) { @@ -30,29 +28,43 @@ func splitReference(_ reference: String) throws -> (String?, String) { } // Split the name into repository and tag parts -// distribution/distribution defines regular expressions which validate names but these seem to be very strict -// and reject names which clients accept -func splitName(_ name: String) throws -> (String, String) { +// distribution/distribution defines regular expressions which validate names +// Some clients, such as docker CLI, accept names which violate these regular expressions for local images, but those images cannot be pushed. +// Other clients, such as podman CLI, reject names which violate these regular expressions even for local images +func parseName(_ name: String) throws -> (ImageReference.Repository, any ImageReference.Reference) { let digestSplit = name.split(separator: "@", maxSplits: 1, omittingEmptySubsequences: false) - if digestSplit.count == 2 { return (String(digestSplit[0]), String(digestSplit[1])) } + if digestSplit.count == 2 { + return ( + try ImageReference.Repository(String(digestSplit[0])), + try ImageReference.Digest(String(digestSplit[1])) + ) + } let tagSplit = name.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) - if tagSplit.count == 0 { throw ImageReference.ValidationError.unexpected("unexpected error") } + if tagSplit.count == 0 { + throw ImageReference.ValidationError.unexpected("unexpected error") + } - if tagSplit.count == 1 { return (name, "latest") } + if tagSplit.count == 1 { + return (try ImageReference.Repository(name), try ImageReference.Tag("latest")) + } // assert splits == 2 - return (String(tagSplit[0]), String(tagSplit[1])) + return ( + try ImageReference.Repository(String(tagSplit[0])), + try ImageReference.Tag(String(tagSplit[1])) + ) } /// ImageReference points to an image stored on a container registry +/// This type is not found in the API - it is the reference string given by the user public struct ImageReference: Sendable, Equatable, CustomStringConvertible, CustomDebugStringConvertible { /// The registry which contains this image public var registry: String /// The repository which contains this image public var repository: Repository - /// The tag identifying the image. - public var reference: String + /// The tag or digest identifying the image. + public var reference: Reference public enum ValidationError: Error { case unexpected(String) @@ -65,7 +77,7 @@ public struct ImageReference: Sendable, Equatable, CustomStringConvertible, Cust /// - Throws: If `reference` cannot be parsed as an image reference. public init(fromString reference: String, defaultRegistry: String = "localhost:5000") throws { let (registry, remainder) = try splitReference(reference) - let (repository, reference) = try splitName(remainder) + let (repository, reference) = try parseName(remainder) self.registry = registry ?? defaultRegistry if self.registry == "docker.io" { self.registry = "index.docker.io" // Special case for docker client, there is no network-level redirect @@ -73,10 +85,10 @@ public struct ImageReference: Sendable, Equatable, CustomStringConvertible, Cust // As a special case, official images can be referred to by a single name, such as `swift` or `swift:slim`. // moby/moby assumes that these names refer to images in `library`: `library/swift` or `library/swift:slim`. // This special case only applies when using Docker Hub, so `example.com/swift` is not expanded `example.com/library/swift` - if self.registry == "index.docker.io" && !repository.contains("/") { + if self.registry == "index.docker.io" && !repository.value.contains("/") { self.repository = try Repository("library/\(repository)") } else { - self.repository = try Repository(repository) + self.repository = repository } self.reference = reference } @@ -87,19 +99,19 @@ public struct ImageReference: Sendable, Equatable, CustomStringConvertible, Cust /// - registry: The registry which stores the image data. /// - repository: The repository within the registry which holds the image. /// - reference: The tag identifying the image. - init(registry: String, repository: Repository, reference: String) { + init(registry: String, repository: Repository, reference: Reference) { self.registry = registry self.repository = repository self.reference = reference } + public static func == (lhs: ImageReference, rhs: ImageReference) -> Bool { + "\(lhs)" == "\(rhs)" + } + /// Printable description of an ImageReference in a form which can be understood by a runtime public var description: String { - if reference.starts(with: "sha256") { - return "\(registry)/\(repository)@\(reference)" - } else { - return "\(registry)/\(repository):\(reference)" - } + "\(registry)/\(repository)\(reference.separator)\(reference)" } /// Printable description of an ImageReference in a form suitable for debugging. @@ -149,3 +161,98 @@ extension ImageReference { } } } + +extension ImageReference { + /// Reference refers to an image in a repository. It can either be a tag or a digest. + public protocol Reference: Sendable, CustomStringConvertible, CustomDebugStringConvertible { + var separator: String { get } + } + + /// Tag is a human-readable name for an image. + public struct Tag: Reference, Sendable, Equatable, CustomStringConvertible, CustomDebugStringConvertible { + var value: String + + public enum ValidationError: Error, Equatable { + case emptyString + case invalidReferenceFormat(String) + case tooLong(String) + } + + public init(_ rawValue: String) throws { + guard rawValue.count > 0 else { + throw ValidationError.emptyString + } + + guard rawValue.count <= 128 else { + throw ValidationError.tooLong(rawValue) + } + + // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests + let regex = /[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}/ + if try regex.wholeMatch(in: rawValue) == nil { + throw ValidationError.invalidReferenceFormat(rawValue) + } + + value = rawValue + } + + public static func == (lhs: Tag, rhs: Tag) -> Bool { + lhs.value == rhs.value + } + + public var separator: String = ":" + + public var description: String { + "\(value)" + } + + /// Printable description in a form suitable for debugging. + public var debugDescription: String { + "Tag(\(value))" + } + } + + /// Digest identifies a specific blob by the hash of the blob's contents. + public struct Digest: Reference, Sendable, Equatable, CustomStringConvertible, CustomDebugStringConvertible { + var value: String + + public enum ValidationError: Error, Equatable { + case emptyString + case invalidReferenceFormat(String) + case tooLong(String) + } + + public init(_ rawValue: String) throws { + guard rawValue.count > 0 else { + throw ValidationError.emptyString + } + + if rawValue.count > 7 + 64 { + throw ValidationError.tooLong(rawValue) + } + + // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests + let regex = /sha256:[a-fA-F0-9]{64}/ + if try regex.wholeMatch(in: rawValue) == nil { + throw ValidationError.invalidReferenceFormat(rawValue) + } + + value = rawValue + } + + public static func == (lhs: Digest, rhs: Digest) -> Bool { + lhs.value == rhs.value + } + + public var separator: String = "@" + + public var description: String { + "\(value)" + } + + /// Printable description in a form suitable for debugging. + public var debugDescription: String { + "Digest(\(value))" + } + } +} diff --git a/Sources/ContainerRegistry/Manifests.swift b/Sources/ContainerRegistry/Manifests.swift index 2ca3834..ac8ccaf 100644 --- a/Sources/ContainerRegistry/Manifests.swift +++ b/Sources/ContainerRegistry/Manifests.swift @@ -13,12 +13,14 @@ //===----------------------------------------------------------------------===// public extension RegistryClient { - func putManifest(repository: ImageReference.Repository, reference: String, manifest: ImageManifest) async throws + func putManifest( + repository: ImageReference.Repository, + reference: any ImageReference.Reference, + manifest: ImageManifest + ) async throws -> String { // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests - precondition("\(reference)".count > 0, "reference must not be an empty string") - let httpResponse = try await executeRequestThrowing( // All blob uploads have Content-Type: application/octet-stream on the wire, even if mediatype is different .put( @@ -42,11 +44,11 @@ public extension RegistryClient { .absoluteString } - func getManifest(repository: ImageReference.Repository, reference: String) async throws -> ImageManifest { + func getManifest(repository: ImageReference.Repository, reference: any ImageReference.Reference) async throws + -> ImageManifest + { // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests - precondition(reference.count > 0, "reference must not be an empty string") - - return try await executeRequestThrowing( + try await executeRequestThrowing( .get( repository, path: "manifests/\(reference)", @@ -60,10 +62,11 @@ public extension RegistryClient { .data } - func getIndex(repository: ImageReference.Repository, reference: String) async throws -> ImageIndex { - precondition(reference.count > 0, "reference must not be an empty string") - - return try await executeRequestThrowing( + func getIndex(repository: ImageReference.Repository, reference: any ImageReference.Reference) async throws + -> ImageIndex + { + // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests + try await executeRequestThrowing( .get( repository, path: "manifests/\(reference)", diff --git a/Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift b/Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift index 64e141d..dc91cf3 100644 --- a/Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift +++ b/Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift @@ -21,7 +21,8 @@ extension RegistryClient { /// - Throws: If the blob cannot be decoded as an `ImageConfiguration`. /// /// Image configuration records are stored as blobs in the registry. This function retrieves the requested blob and tries to decode it as a configuration record. - public func getImageConfiguration(forImage image: ImageReference, digest: String) async throws -> ImageConfiguration + public func getImageConfiguration(forImage image: ImageReference, digest: ImageReference.Digest) async throws + -> ImageConfiguration { try await getBlob(repository: image.repository, digest: digest) } diff --git a/Sources/containertool/Extensions/Errors+CustomStringConvertible.swift b/Sources/containertool/Extensions/Errors+CustomStringConvertible.swift index 066bd98..aeeb7ff 100644 --- a/Sources/containertool/Extensions/Errors+CustomStringConvertible.swift +++ b/Sources/containertool/Extensions/Errors+CustomStringConvertible.swift @@ -60,3 +60,31 @@ extension ContainerRegistry.ImageReference.Repository.ValidationError: Swift.Cus } } } + +extension ContainerRegistry.ImageReference.Tag.ValidationError: Swift.CustomStringConvertible { + /// A human-readable string describing an image reference validation error + public var description: String { + switch self { + case .emptyString: + return "Invalid reference format: tag cannot be empty" + case .tooLong(let rawValue): + return "Invalid reference format: tag (\(rawValue)) is too long" + case .invalidReferenceFormat(let rawValue): + return "Invalid reference format: tag (\(rawValue)) contains invalid characters" + } + } +} + +extension ContainerRegistry.ImageReference.Digest.ValidationError: Swift.CustomStringConvertible { + /// A human-readable string describing an image reference validation error + public var description: String { + switch self { + case .emptyString: + return "Invalid reference format: digest cannot be empty" + case .tooLong(let rawValue): + return "Invalid reference format: digest (\(rawValue)) is too long" + case .invalidReferenceFormat(let rawValue): + return "Invalid reference format: digest (\(rawValue)) is not a valid digest" + } + } +} diff --git a/Sources/containertool/Extensions/RegistryClient+CopyBlobs.swift b/Sources/containertool/Extensions/RegistryClient+CopyBlobs.swift index db229ca..09dcf48 100644 --- a/Sources/containertool/Extensions/RegistryClient+CopyBlobs.swift +++ b/Sources/containertool/Extensions/RegistryClient+CopyBlobs.swift @@ -23,7 +23,7 @@ extension RegistryClient { /// - destRepository: The repository on this registry to which the blob should be copied. /// - Throws: If the copy cannot be completed. func copyBlob( - digest: String, + digest: ImageReference.Digest, fromRepository sourceRepository: ImageReference.Repository, toClient destClient: RegistryClient, toRepository destRepository: ImageReference.Repository @@ -39,6 +39,6 @@ extension RegistryClient { log("Layer \(digest): pushing") let uploaded = try await destClient.putBlob(repository: destRepository, data: blob) log("Layer \(digest): done") - assert(digest == uploaded.digest) + assert("\(digest)" == uploaded.digest) } } diff --git a/Sources/containertool/Extensions/RegistryClient+Layers.swift b/Sources/containertool/Extensions/RegistryClient+Layers.swift index 6a2970e..167fc04 100644 --- a/Sources/containertool/Extensions/RegistryClient+Layers.swift +++ b/Sources/containertool/Extensions/RegistryClient+Layers.swift @@ -26,13 +26,16 @@ extension RegistryClient { return try await getManifest(repository: image.repository, reference: image.reference) } catch { // Try again, treating the top level object as an index. - // This could be more efficient if the exception thrown by getManfiest() included the data it was unable to parse + // This could be more efficient if the exception thrown by getManifest() included the data it was unable to parse let index = try await getIndex(repository: image.repository, reference: image.reference) guard let manifest = index.manifests.first(where: { $0.platform?.architecture == architecture }) else { throw "Could not find a suitable base image for \(architecture)" } // The index should not point to another index; if it does, this call will throw a final error to be handled by the caller. - return try await getManifest(repository: image.repository, reference: manifest.digest) + return try await getManifest( + repository: image.repository, + reference: ImageReference.Digest(manifest.digest) + ) } } diff --git a/Sources/containertool/containertool.swift b/Sources/containertool/containertool.swift index 148c479..4ac4ed6 100644 --- a/Sources/containertool/containertool.swift +++ b/Sources/containertool/containertool.swift @@ -176,7 +176,7 @@ extension RegistryClient { baseImageConfiguration = try await source.getImageConfiguration( forImage: baseImage, - digest: baseImageManifest.config.digest + digest: ImageReference.Digest(baseImageManifest.config.digest) ) log("Found base image configuration: \(baseImageManifest.config.digest)") } else { @@ -276,7 +276,7 @@ extension RegistryClient { if let source { for layer in baseImageManifest.layers { try await source.copyBlob( - digest: layer.digest, + digest: ImageReference.Digest(layer.digest), fromRepository: baseImage.repository, toClient: self, toRepository: destinationImage.repository @@ -289,7 +289,12 @@ extension RegistryClient { // 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 = tag ?? manifest.digest + let reference: ImageReference.Reference + if let tag { + reference = try ImageReference.Tag(tag) + } else { + reference = try ImageReference.Digest(manifest.digest) + } let location = try await self.putManifest( repository: destinationImage.repository, reference: destinationImage.reference, diff --git a/Tests/ContainerRegistryTests/ImageReferenceTests.swift b/Tests/ContainerRegistryTests/ImageReferenceTests.swift index 0983c88..06dce30 100644 --- a/Tests/ContainerRegistryTests/ImageReferenceTests.swift +++ b/Tests/ContainerRegistryTests/ImageReferenceTests.swift @@ -29,7 +29,7 @@ struct ReferenceTests { expected: try! ImageReference( registry: "default", repository: ImageReference.Repository("localhost"), - reference: "latest" + reference: ImageReference.Tag("latest") ) ), ReferenceTestCase( @@ -37,7 +37,7 @@ struct ReferenceTests { expected: try! ImageReference( registry: "default", repository: ImageReference.Repository("example.com"), - reference: "latest" + reference: ImageReference.Tag("latest") ) ), ReferenceTestCase( @@ -45,7 +45,7 @@ struct ReferenceTests { expected: try! ImageReference( registry: "default", repository: ImageReference.Repository("example"), - reference: "1234" + reference: ImageReference.Tag("1234") ) ), @@ -60,7 +60,7 @@ struct ReferenceTests { expected: try! ImageReference( registry: "localhost", repository: ImageReference.Repository("foo"), - reference: "latest" + reference: ImageReference.Tag("latest") ) ), ReferenceTestCase( @@ -68,7 +68,7 @@ struct ReferenceTests { expected: try! ImageReference( registry: "localhost:1234", repository: ImageReference.Repository("foo"), - reference: "latest" + reference: ImageReference.Tag("latest") ) ), ReferenceTestCase( @@ -76,7 +76,7 @@ struct ReferenceTests { expected: try! ImageReference( registry: "example.com", repository: ImageReference.Repository("foo"), - reference: "latest" + reference: ImageReference.Tag("latest") ) ), ReferenceTestCase( @@ -84,7 +84,7 @@ struct ReferenceTests { expected: try! ImageReference( registry: "example.com:1234", repository: ImageReference.Repository("foo"), - reference: "latest" + reference: ImageReference.Tag("latest") ) ), ReferenceTestCase( @@ -92,7 +92,7 @@ struct ReferenceTests { expected: try! ImageReference( registry: "example.com:1234", repository: ImageReference.Repository("foo"), - reference: "bar" + reference: ImageReference.Tag("bar") ) ), @@ -103,7 +103,7 @@ struct ReferenceTests { expected: try! ImageReference( registry: "default", repository: ImageReference.Repository("local/foo"), - reference: "latest" + reference: ImageReference.Tag("latest") ) ), ReferenceTestCase( @@ -111,7 +111,7 @@ struct ReferenceTests { expected: try! ImageReference( registry: "default", repository: ImageReference.Repository("example/foo"), - reference: "latest" + reference: ImageReference.Tag("latest") ) ), ReferenceTestCase( @@ -119,35 +119,28 @@ struct ReferenceTests { expected: try! ImageReference( registry: "default", repository: ImageReference.Repository("example/foo"), - reference: "1234" + reference: ImageReference.Tag("1234") ) ), // Distribution spec tests ReferenceTestCase( - reference: "example.com/foo@sha256:0123456789abcdef01234567890abcdef", + reference: "example.com/foo@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", expected: try! ImageReference( registry: "example.com", repository: ImageReference.Repository("foo"), - reference: "sha256:0123456789abcdef01234567890abcdef" + reference: ImageReference.Digest( + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + ) ) ), - // This example goes against the distribution spec's regular expressions but matches observed client behaviour ReferenceTestCase( reference: "foo:1234/bar:1234", expected: try! ImageReference( registry: "foo:1234", repository: ImageReference.Repository("bar"), - reference: "1234" - ) - ), - ReferenceTestCase( - reference: "localhost/foo:1234/bar:1234", - expected: try! ImageReference( - registry: "localhost", - repository: ImageReference.Repository("foo"), - reference: "1234/bar:1234" + reference: ImageReference.Tag("1234") ) ), @@ -157,7 +150,7 @@ struct ReferenceTests { expected: try! ImageReference( registry: "EXAMPLE.COM", repository: ImageReference.Repository("foo"), - reference: "latest" + reference: ImageReference.Tag("latest") ) ), ] @@ -205,7 +198,7 @@ struct ReferenceTests { == ImageReference( registry: "index.docker.io", repository: ImageReference.Repository("library/swift"), - reference: "slim" + reference: ImageReference.Tag("slim") ) ) @@ -215,7 +208,7 @@ struct ReferenceTests { == ImageReference( registry: "index.docker.io", repository: ImageReference.Repository("library/swift"), - reference: "slim" + reference: ImageReference.Tag("slim") ) ) @@ -225,7 +218,7 @@ struct ReferenceTests { == ImageReference( registry: "index.docker.io", repository: ImageReference.Repository("library/swift"), - reference: "slim" + reference: ImageReference.Tag("slim") ) ) @@ -235,7 +228,7 @@ struct ReferenceTests { == ImageReference( registry: "index.docker.io", repository: ImageReference.Repository("library/swift"), - reference: "slim" + reference: ImageReference.Tag("slim") ) ) @@ -245,7 +238,7 @@ struct ReferenceTests { == ImageReference( registry: "index.docker.io", repository: ImageReference.Repository("library/swift"), - reference: "latest" + reference: ImageReference.Tag("latest") ) ) @@ -255,7 +248,7 @@ struct ReferenceTests { == ImageReference( registry: "localhost:5000", repository: ImageReference.Repository("swift"), - reference: "latest" + reference: ImageReference.Tag("latest") ) ) @@ -264,7 +257,7 @@ struct ReferenceTests { == ImageReference( registry: "localhost:5000", repository: ImageReference.Repository("swift"), - reference: "latest" + reference: ImageReference.Tag("latest") ) ) } diff --git a/Tests/ContainerRegistryTests/SmokeTests.swift b/Tests/ContainerRegistryTests/SmokeTests.swift index 1754ead..7115d14 100644 --- a/Tests/ContainerRegistryTests/SmokeTests.swift +++ b/Tests/ContainerRegistryTests/SmokeTests.swift @@ -58,14 +58,18 @@ struct SmokeTests { ) // After setting a tag, we should be able to retrieve it - let _ = try await client.putManifest(repository: repository, reference: "latest", manifest: test_manifest) + let _ = try await client.putManifest( + repository: repository, + reference: ImageReference.Tag("latest"), + manifest: test_manifest + ) let firstTag = try await client.getTags(repository: repository).tags.sorted() #expect(firstTag == ["latest"]) // After setting another tag, the original tag should still exist let _ = try await client.putManifest( repository: repository, - reference: "additional_tag", + reference: ImageReference.Tag("additional_tag"), manifest: test_manifest ) let secondTag = try await client.getTags(repository: repository) @@ -78,7 +82,7 @@ struct SmokeTests { do { let _ = try await client.getBlob( repository: repository, - digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + digest: ImageReference.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") ) Issue.record("should have thrown") } catch {} @@ -89,7 +93,7 @@ struct SmokeTests { let exists = try await client.blobExists( repository: repository, - digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + digest: ImageReference.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") ) #expect(!exists) } @@ -102,10 +106,13 @@ struct SmokeTests { let descriptor = try await client.putBlob(repository: repository, data: blob_data) #expect(descriptor.digest == "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08") - let exists = try await client.blobExists(repository: repository, digest: descriptor.digest) + let exists = try await client.blobExists( + repository: repository, + digest: ImageReference.Digest(descriptor.digest) + ) #expect(exists) - let blob = try await client.getBlob(repository: repository, digest: descriptor.digest) + let blob = try await client.getBlob(repository: repository, digest: ImageReference.Digest(descriptor.digest)) #expect(blob == blob_data) } @@ -135,9 +142,13 @@ struct SmokeTests { layers: [image_descriptor] ) - let _ = try await client.putManifest(repository: repository, reference: "latest", manifest: test_manifest) + let _ = try await client.putManifest( + repository: repository, + reference: ImageReference.Tag("latest"), + manifest: test_manifest + ) - let manifest = try await client.getManifest(repository: repository, reference: "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) @@ -172,11 +183,14 @@ struct SmokeTests { let _ = try await client.putManifest( repository: repository, - reference: test_manifest.digest, + reference: ImageReference.Digest(test_manifest.digest), manifest: test_manifest ) - let manifest = try await client.getManifest(repository: repository, reference: test_manifest.digest) + let manifest = try await client.getManifest( + repository: repository, + reference: ImageReference.Digest(test_manifest.digest) + ) #expect(manifest.schemaVersion == 2) #expect(manifest.config.mediaType == "application/vnd.docker.container.image.v1+json") #expect(manifest.layers.count == 1) @@ -185,7 +199,11 @@ struct SmokeTests { @Test func testPutAndGetImageConfiguration() async throws { let repository = try ImageReference.Repository("testputandgetimageconfiguration") - let image = ImageReference(registry: "registry", repository: repository, reference: "latest") + let image = try ImageReference( + registry: "registry", + repository: repository, + reference: ImageReference.Tag("latest") + ) let configuration = ImageConfiguration( created: "1996-12-19T16:39:57-08:00", @@ -202,7 +220,7 @@ struct SmokeTests { let downloaded = try await client.getImageConfiguration( forImage: image, - digest: config_descriptor.digest + digest: ImageReference.Digest(config_descriptor.digest) ) #expect(configuration == downloaded)