From db37b0c3dacc8c95d69b4c7ffc9a44f3036be9ce Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Mon, 9 Jun 2025 11:37:59 +0100 Subject: [PATCH] ContainerRegistry: Reject invalid image tags and digests (#140) Motivation ---------- `ImageReference` does not check for illegal characters in parsed image digests and tags. This means that `containertool` will send illegal image names to the registry. The registry will reject them, but the error message might not explain why, so a generic error message will be printed. Runtimes reject illegal image references immediately, without sending them to the registry. Some desktop runtimes accept local image names which the registry will reject; other runtimes reject these names even for local names. `containertool` now also rejects them. Modifications ------------- * Check validity of tags and digests when parsing image names * Change the low-level API functions to accept `Digest` or `Reference` instead of `String`. Result ------ It is impossible to create a `Repository` object containing a malformed tag or digest, because the constructor checks the string value. It is impossible to send a malformed name to the registry because the API wrappers only accept `Digest` or `Reference (Digest | Tag)` objects. Fixes #139 Test Plan --------- Existing tests continue to pass. New tests exercise additional checks which were previously missing. Removed tests which checked tags which seemed to be accepted by some desktop runtimes, but which were not accepted by registries. --- Sources/ContainerRegistry/Blobs.swift | 18 +-- .../ContainerRegistry/ImageReference.swift | 147 +++++++++++++++--- Sources/ContainerRegistry/Manifests.swift | 25 +-- .../RegistryClient+ImageConfiguration.swift | 3 +- .../Errors+CustomStringConvertible.swift | 28 ++++ .../Extensions/RegistryClient+CopyBlobs.swift | 4 +- .../Extensions/RegistryClient+Layers.swift | 7 +- Sources/containertool/containertool.swift | 11 +- .../ImageReferenceTests.swift | 55 +++---- Tests/ContainerRegistryTests/SmokeTests.swift | 42 +++-- 10 files changed, 247 insertions(+), 93 deletions(-) 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)