diff --git a/Sources/ContainerRegistry/AuthHandler.swift b/Sources/ContainerRegistry/AuthHandler.swift index cd45cae..245a725 100644 --- a/Sources/ContainerRegistry/AuthHandler.swift +++ b/Sources/ContainerRegistry/AuthHandler.swift @@ -158,7 +158,7 @@ public struct AuthHandler { public func auth( registry: URL, - repository: String, + repository: ImageReference.Repository, actions: [String], withScheme scheme: AuthChallenge, usingClient client: HTTPClient diff --git a/Sources/ContainerRegistry/Blobs.swift b/Sources/ContainerRegistry/Blobs.swift index 3c13bed..ea9edaf 100644 --- a/Sources/ContainerRegistry/Blobs.swift +++ b/Sources/ContainerRegistry/Blobs.swift @@ -28,9 +28,7 @@ public func digest(of data: D) -> String { extension RegistryClient { // Internal helper method to initiate a blob upload in 'two shot' mode - func startBlobUploadSession(repository: String) async throws -> URL { - precondition(repository.count > 0, "repository must not be an empty string") - + func startBlobUploadSession(repository: ImageReference.Repository) async throws -> URL { // Upload in "two shot" mode. // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#post-then-put // - POST to obtain a session ID. @@ -67,8 +65,7 @@ extension RegistryClient { extension HTTPField.Name { static let dockerContentDigest = Self("Docker-Content-Digest")! } public extension RegistryClient { - func blobExists(repository: String, digest: String) async throws -> Bool { - precondition(repository.count > 0, "repository must not be an empty string") + func blobExists(repository: ImageReference.Repository, digest: String) async throws -> Bool { precondition(digest.count > 0) do { @@ -87,8 +84,7 @@ public extension RegistryClient { /// - digest: Digest of the blob. /// - Returns: The downloaded data. /// - Throws: If the blob download fails. - func getBlob(repository: String, digest: String) async throws -> Data { - precondition(repository.count > 0, "repository must not be an empty string") + 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( @@ -110,8 +106,7 @@ 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: String, digest: String) async throws -> Response { - precondition(repository.count > 0, "repository must not be an empty string") + 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( @@ -132,11 +127,10 @@ public extension RegistryClient { /// - Returns: An ContentDescriptor object representing the /// uploaded blob. /// - Throws: If the blob cannot be encoded or the upload fails. - func putBlob(repository: String, mediaType: String = "application/octet-stream", data: Data) async throws + func putBlob(repository: ImageReference.Repository, mediaType: String = "application/octet-stream", data: Data) + async throws -> ContentDescriptor { - precondition(repository.count > 0, "repository must not be an empty string") - // Ask the server to open a session and tell us where to upload our data let location = try await startBlobUploadSession(repository: repository) @@ -179,7 +173,11 @@ public extension RegistryClient { /// Some JSON objects, such as ImageConfiguration, are stored /// in the registry as plain blobs with MIME type "application/octet-stream". /// This function encodes the data parameter and uploads it as a generic blob. - func putBlob(repository: String, mediaType: String = "application/octet-stream", data: Body) + func putBlob( + repository: ImageReference.Repository, + mediaType: String = "application/octet-stream", + data: Body + ) async throws -> ContentDescriptor { let encoded = try encoder.encode(data) diff --git a/Sources/ContainerRegistry/ImageReference.swift b/Sources/ContainerRegistry/ImageReference.swift index 1fed795..5660635 100644 --- a/Sources/ContainerRegistry/ImageReference.swift +++ b/Sources/ContainerRegistry/ImageReference.swift @@ -14,13 +14,11 @@ import RegexBuilder -enum ReferenceError: Error { case unexpected(String) } - // 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) { let splits = reference.split(separator: "/", maxSplits: 1, omittingEmptySubsequences: false) - if splits.count == 0 { throw ReferenceError.unexpected("unexpected error") } + if splits.count == 0 { throw ImageReference.ValidationError.unexpected("unexpected error") } if splits.count == 1 { return (nil, reference) } @@ -39,7 +37,7 @@ func splitName(_ name: String) throws -> (String, String) { if digestSplit.count == 2 { return (String(digestSplit[0]), String(digestSplit[1])) } let tagSplit = name.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) - if tagSplit.count == 0 { throw ReferenceError.unexpected("unexpected error") } + if tagSplit.count == 0 { throw ImageReference.ValidationError.unexpected("unexpected error") } if tagSplit.count == 1 { return (name, "latest") } @@ -52,10 +50,14 @@ public struct ImageReference: Sendable, Equatable, CustomStringConvertible, Cust /// The registry which contains this image public var registry: String /// The repository which contains this image - public var repository: String + public var repository: Repository /// The tag identifying the image. public var reference: String + public enum ValidationError: Error { + case unexpected(String) + } + /// Creates an ImageReference from an image reference string. /// - Parameters: /// - reference: The reference to parse. @@ -72,19 +74,20 @@ public struct ImageReference: Sendable, Equatable, CustomStringConvertible, Cust // 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("/") { - self.repository = "library/\(repository)" + self.repository = try Repository("library/\(repository)") } else { - self.repository = repository + self.repository = try Repository(repository) } self.reference = reference } /// Creates an ImageReference from separate registry, repository and reference strings. + /// Used only in tests. /// - Parameters: /// - registry: The registry which stores the image data. /// - repository: The repository within the registry which holds the image. /// - reference: The tag identifying the image. - public init(registry: String, repository: String, reference: String) { + init(registry: String, repository: Repository, reference: String) { self.registry = registry self.repository = repository self.reference = reference @@ -104,3 +107,45 @@ public struct ImageReference: Sendable, Equatable, CustomStringConvertible, Cust "ImageReference(registry: \(registry), repository: \(repository), reference: \(reference))" } } + +extension ImageReference { + /// Repository refers a repository (image namespace) on a container registry + public struct Repository: Sendable, Equatable, CustomStringConvertible, CustomDebugStringConvertible { + var value: String + + public enum ValidationError: Error, Equatable { + case emptyString + case containsUppercaseLetters(String) + case invalidReferenceFormat(String) + } + + public init(_ rawValue: String) throws { + // Reference handling in github.com/distribution reports empty and uppercase as specific errors. + // All other errors caused are reported as generic format errors. + guard rawValue.count > 0 else { + throw ValidationError.emptyString + } + + if (rawValue.contains { $0.isUppercase }) { + throw ValidationError.containsUppercaseLetters(rawValue) + } + + // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests + let regex = /[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*/ + if try regex.wholeMatch(in: rawValue) == nil { + throw ValidationError.invalidReferenceFormat(rawValue) + } + + value = rawValue + } + + public var description: String { + value + } + + /// Printable description of an ImageReference in a form suitable for debugging. + public var debugDescription: String { + "Repository(\(value))" + } + } +} diff --git a/Sources/ContainerRegistry/Manifests.swift b/Sources/ContainerRegistry/Manifests.swift index b00001a..2ca3834 100644 --- a/Sources/ContainerRegistry/Manifests.swift +++ b/Sources/ContainerRegistry/Manifests.swift @@ -13,10 +13,11 @@ //===----------------------------------------------------------------------===// public extension RegistryClient { - func putManifest(repository: String, reference: String, manifest: ImageManifest) async throws -> String { + func putManifest(repository: ImageReference.Repository, reference: String, manifest: ImageManifest) async throws + -> String + { // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests - precondition(repository.count > 0, "repository must not be an empty string") - precondition(reference.count > 0, "reference must not be an empty string") + 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 @@ -41,9 +42,8 @@ public extension RegistryClient { .absoluteString } - func getManifest(repository: String, reference: String) async throws -> ImageManifest { + func getManifest(repository: ImageReference.Repository, reference: String) async throws -> ImageManifest { // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests - precondition(repository.count > 0, "repository must not be an empty string") precondition(reference.count > 0, "reference must not be an empty string") return try await executeRequestThrowing( @@ -60,8 +60,7 @@ public extension RegistryClient { .data } - func getIndex(repository: String, reference: String) async throws -> ImageIndex { - precondition(repository.count > 0, "repository must not be an empty string") + 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( diff --git a/Sources/ContainerRegistry/RegistryClient.swift b/Sources/ContainerRegistry/RegistryClient.swift index 2d2bc04..beaf60c 100644 --- a/Sources/ContainerRegistry/RegistryClient.swift +++ b/Sources/ContainerRegistry/RegistryClient.swift @@ -127,7 +127,8 @@ extension URL { /// - repository: The name of the repository. May include path separators. /// - endpoint: The distribution endpoint e.g. "tags/list" /// - Returns: A fully-qualified URL for the endpoint. - func distributionEndpoint(forRepository repository: String, andEndpoint endpoint: String) -> URL { + func distributionEndpoint(forRepository repository: ImageReference.Repository, andEndpoint endpoint: String) -> URL + { self.appendingPathComponent("/v2/\(repository)/\(endpoint)") } } @@ -141,7 +142,7 @@ extension RegistryClient { } var method: HTTPRequest.Method // HTTP method - var repository: String // Repository path on the registry + var repository: ImageReference.Repository // Repository path on the registry var destination: Destination // Destination of the operation: can be a subpath or remote URL var actions: [String] // Actions required by this operation var accepting: [String] = [] // Acceptable response types @@ -156,7 +157,7 @@ extension RegistryClient { // Convenience constructors static func get( - _ repository: String, + _ repository: ImageReference.Repository, path: String, actions: [String]? = nil, accepting: [String] = [], @@ -173,7 +174,7 @@ extension RegistryClient { } static func get( - _ repository: String, + _ repository: ImageReference.Repository, url: URL, actions: [String]? = nil, accepting: [String] = [], @@ -190,7 +191,7 @@ extension RegistryClient { } static func head( - _ repository: String, + _ repository: ImageReference.Repository, path: String, actions: [String]? = nil, accepting: [String] = [], @@ -208,7 +209,7 @@ extension RegistryClient { /// This handles the 'put' case where the registry gives us a location URL which we must not alter, aside from adding the digest to it static func put( - _ repository: String, + _ repository: ImageReference.Repository, url: URL, actions: [String]? = nil, accepting: [String] = [], @@ -225,7 +226,7 @@ extension RegistryClient { } static func put( - _ repository: String, + _ repository: ImageReference.Repository, path: String, actions: [String]? = nil, accepting: [String] = [], @@ -242,7 +243,7 @@ extension RegistryClient { } static func post( - _ repository: String, + _ repository: ImageReference.Repository, path: String, actions: [String]? = nil, accepting: [String] = [], diff --git a/Sources/ContainerRegistry/Tags.swift b/Sources/ContainerRegistry/Tags.swift index 910b6c4..59f6f87 100644 --- a/Sources/ContainerRegistry/Tags.swift +++ b/Sources/ContainerRegistry/Tags.swift @@ -13,10 +13,8 @@ //===----------------------------------------------------------------------===// public extension RegistryClient { - func getTags(repository: String) async throws -> Tags { + func getTags(repository: ImageReference.Repository) async throws -> Tags { // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-tags - precondition(repository.count > 0, "repository must not be an empty string") - - return try await executeRequestThrowing(.get(repository, path: "tags/list"), decodingErrors: [.notFound]).data + try await executeRequestThrowing(.get(repository, path: "tags/list"), decodingErrors: [.notFound]).data } } diff --git a/Sources/containertool/Extensions/Errors+CustomStringConvertible.swift b/Sources/containertool/Extensions/Errors+CustomStringConvertible.swift index 337473a..066bd98 100644 --- a/Sources/containertool/Extensions/Errors+CustomStringConvertible.swift +++ b/Sources/containertool/Extensions/Errors+CustomStringConvertible.swift @@ -46,3 +46,17 @@ extension ContainerRegistry.DistributionErrors: Swift.CustomStringConvertible { /// A human-readable string describing a collection of unhandled distribution protocol errors public var description: String { errors.map { $0.description }.joined(separator: "\n") } } + +extension ContainerRegistry.ImageReference.Repository.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: repository name cannot be empty" + case .containsUppercaseLetters(let rawValue): + return "Invalid reference format: repository name (\(rawValue)) must be lowercase" + case .invalidReferenceFormat(let rawValue): + return "Invalid reference format: repository name (\(rawValue)) contains invalid characters" + } + } +} diff --git a/Sources/containertool/Extensions/RegistryClient+CopyBlobs.swift b/Sources/containertool/Extensions/RegistryClient+CopyBlobs.swift index c536248..db229ca 100644 --- a/Sources/containertool/Extensions/RegistryClient+CopyBlobs.swift +++ b/Sources/containertool/Extensions/RegistryClient+CopyBlobs.swift @@ -24,9 +24,9 @@ extension RegistryClient { /// - Throws: If the copy cannot be completed. func copyBlob( digest: String, - fromRepository sourceRepository: String, + fromRepository sourceRepository: ImageReference.Repository, toClient destClient: RegistryClient, - toRepository destRepository: String + toRepository destRepository: ImageReference.Repository ) async throws { if try await destClient.blobExists(repository: destRepository, digest: digest) { log("Layer \(digest): already exists") diff --git a/Sources/containertool/Extensions/RegistryClient+Layers.swift b/Sources/containertool/Extensions/RegistryClient+Layers.swift index ccc79b1..6a2970e 100644 --- a/Sources/containertool/Extensions/RegistryClient+Layers.swift +++ b/Sources/containertool/Extensions/RegistryClient+Layers.swift @@ -45,7 +45,7 @@ extension RegistryClient { // A layer is a tarball, optionally compressed using gzip or zstd // See https://github.com/opencontainers/image-spec/blob/main/media-types.md func uploadLayer( - repository: String, + repository: ImageReference.Repository, contents: [UInt8], mediaType: String = "application/vnd.oci.image.layer.v1.tar+gzip" ) async throws -> ImageLayer { diff --git a/Tests/ContainerRegistryTests/ImageReferenceTests.swift b/Tests/ContainerRegistryTests/ImageReferenceTests.swift index 46f9c90..0983c88 100644 --- a/Tests/ContainerRegistryTests/ImageReferenceTests.swift +++ b/Tests/ContainerRegistryTests/ImageReferenceTests.swift @@ -17,7 +17,7 @@ import Testing struct ReferenceTestCase: Sendable { var reference: String - var expected: ImageReference? + var expected: ImageReference } struct ReferenceTests { @@ -26,15 +26,27 @@ struct ReferenceTests { // in the default registry. ReferenceTestCase( reference: "localhost", - expected: ImageReference(registry: "default", repository: "localhost", reference: "latest") + expected: try! ImageReference( + registry: "default", + repository: ImageReference.Repository("localhost"), + reference: "latest" + ) ), ReferenceTestCase( reference: "example.com", - expected: ImageReference(registry: "default", repository: "example.com", reference: "latest") + expected: try! ImageReference( + registry: "default", + repository: ImageReference.Repository("example.com"), + reference: "latest" + ) ), ReferenceTestCase( reference: "example:1234", - expected: ImageReference(registry: "default", repository: "example", reference: "1234") + expected: try! ImageReference( + registry: "default", + repository: ImageReference.Repository("example"), + reference: "1234" + ) ), // If a reference contains a '/' *and* the component before the '/' looks like a @@ -45,46 +57,78 @@ struct ReferenceTests { // "localhost" is a special case. ReferenceTestCase( reference: "localhost/foo", - expected: ImageReference(registry: "localhost", repository: "foo", reference: "latest") + expected: try! ImageReference( + registry: "localhost", + repository: ImageReference.Repository("foo"), + reference: "latest" + ) ), ReferenceTestCase( reference: "localhost:1234/foo", - expected: ImageReference(registry: "localhost:1234", repository: "foo", reference: "latest") + expected: try! ImageReference( + registry: "localhost:1234", + repository: ImageReference.Repository("foo"), + reference: "latest" + ) ), ReferenceTestCase( reference: "example.com/foo", - expected: ImageReference(registry: "example.com", repository: "foo", reference: "latest") + expected: try! ImageReference( + registry: "example.com", + repository: ImageReference.Repository("foo"), + reference: "latest" + ) ), ReferenceTestCase( reference: "example.com:1234/foo", - expected: ImageReference(registry: "example.com:1234", repository: "foo", reference: "latest") + expected: try! ImageReference( + registry: "example.com:1234", + repository: ImageReference.Repository("foo"), + reference: "latest" + ) ), ReferenceTestCase( reference: "example.com:1234/foo:bar", - expected: ImageReference(registry: "example.com:1234", repository: "foo", reference: "bar") + expected: try! ImageReference( + registry: "example.com:1234", + repository: ImageReference.Repository("foo"), + reference: "bar" + ) ), // If the part before the '/' does not look like a hostname, the whole reference // is interpreted as a repository name in the default registry. ReferenceTestCase( reference: "local/foo", - expected: ImageReference(registry: "default", repository: "local/foo", reference: "latest") + expected: try! ImageReference( + registry: "default", + repository: ImageReference.Repository("local/foo"), + reference: "latest" + ) ), ReferenceTestCase( reference: "example/foo", - expected: ImageReference(registry: "default", repository: "example/foo", reference: "latest") + expected: try! ImageReference( + registry: "default", + repository: ImageReference.Repository("example/foo"), + reference: "latest" + ) ), ReferenceTestCase( reference: "example/foo:1234", - expected: ImageReference(registry: "default", repository: "example/foo", reference: "1234") + expected: try! ImageReference( + registry: "default", + repository: ImageReference.Repository("example/foo"), + reference: "1234" + ) ), // Distribution spec tests ReferenceTestCase( reference: "example.com/foo@sha256:0123456789abcdef01234567890abcdef", - expected: ImageReference( + expected: try! ImageReference( registry: "example.com", - repository: "foo", + repository: ImageReference.Repository("foo"), reference: "sha256:0123456789abcdef01234567890abcdef" ) ), @@ -92,15 +136,34 @@ struct ReferenceTests { // This example goes against the distribution spec's regular expressions but matches observed client behaviour ReferenceTestCase( reference: "foo:1234/bar:1234", - expected: ImageReference(registry: "foo:1234", repository: "bar", reference: "1234") + expected: try! ImageReference( + registry: "foo:1234", + repository: ImageReference.Repository("bar"), + reference: "1234" + ) ), ReferenceTestCase( reference: "localhost/foo:1234/bar:1234", - expected: ImageReference(registry: "localhost", repository: "foo", reference: "1234/bar:1234") + expected: try! ImageReference( + registry: "localhost", + repository: ImageReference.Repository("foo"), + reference: "1234/bar:1234" + ) + ), + + // Capitals are not allowed in repository names but are allowed in hostnames (matching podman's behaviour) + ReferenceTestCase( + reference: "EXAMPLE.COM/foo:latest", + expected: try! ImageReference( + registry: "EXAMPLE.COM", + repository: ImageReference.Repository("foo"), + reference: "latest" + ) ), ] - @Test(arguments: tests) func testReferences(test: ReferenceTestCase) throws { + @Test(arguments: tests) + func testValidReferences(test: ReferenceTestCase) throws { let parsed = try! ImageReference(fromString: test.reference, defaultRegistry: "default") #expect( parsed == test.expected, @@ -108,49 +171,101 @@ struct ReferenceTests { ) } - @Test func testLibraryReferences() throws { + @Test + func testInvalidReferences() throws { + #expect(throws: ImageReference.Repository.ValidationError.emptyString) { + try ImageReference(fromString: "", defaultRegistry: "default") + } + + #expect(throws: ImageReference.Repository.ValidationError.emptyString) { + try ImageReference(fromString: "example.com/") + } + + #expect(throws: ImageReference.Repository.ValidationError.containsUppercaseLetters("helloWorld")) { + try ImageReference(fromString: "helloWorld", defaultRegistry: "default") + } + + #expect(throws: ImageReference.Repository.ValidationError.containsUppercaseLetters("helloWorld")) { + try ImageReference(fromString: "localhost:5555/helloWorld") + } + + #expect(throws: ImageReference.Repository.ValidationError.invalidReferenceFormat("hello^world")) { + try ImageReference(fromString: "localhost:5555/hello^world") + } + } + + @Test + func testLibraryReferences() throws { // docker.io is a special case, as references such as "swift:slim" with no registry component are translated to "docker.io/library/swift:slim" // Verified against the behaviour of the docker CLI client // Fully-qualified name splits as usual #expect( try! ImageReference(fromString: "docker.io/library/swift:slim", defaultRegistry: "docker.io") - == ImageReference(registry: "index.docker.io", repository: "library/swift", reference: "slim") + == ImageReference( + registry: "index.docker.io", + repository: ImageReference.Repository("library/swift"), + reference: "slim" + ) ) // A repository with no '/' part is assumed to be `library` #expect( try! ImageReference(fromString: "docker.io/swift:slim", defaultRegistry: "docker.io") - == ImageReference(registry: "index.docker.io", repository: "library/swift", reference: "slim") + == ImageReference( + registry: "index.docker.io", + repository: ImageReference.Repository("library/swift"), + reference: "slim" + ) ) // Parsing with 'docker.io' as default registry is the same as the fully qualified case #expect( try! ImageReference(fromString: "library/swift:slim", defaultRegistry: "docker.io") - == ImageReference(registry: "index.docker.io", repository: "library/swift", reference: "slim") + == ImageReference( + registry: "index.docker.io", + repository: ImageReference.Repository("library/swift"), + reference: "slim" + ) ) // Bare image name with no registry or repository is interpreted as being in docker.io/library when default is docker.io #expect( try! ImageReference(fromString: "swift:slim", defaultRegistry: "docker.io") - == ImageReference(registry: "index.docker.io", repository: "library/swift", reference: "slim") + == ImageReference( + registry: "index.docker.io", + repository: ImageReference.Repository("library/swift"), + reference: "slim" + ) ) // The minimum reference to a library image. No tag implies `latest` #expect( try! ImageReference(fromString: "swift", defaultRegistry: "docker.io") - == ImageReference(registry: "index.docker.io", repository: "library/swift", reference: "latest") + == ImageReference( + registry: "index.docker.io", + repository: ImageReference.Repository("library/swift"), + reference: "latest" + ) ) // If the registry is not docker.io, the special case logic for `library` does not apply #expect( try! ImageReference(fromString: "localhost:5000/swift", defaultRegistry: "docker.io") - == ImageReference(registry: "localhost:5000", repository: "swift", reference: "latest") + == ImageReference( + registry: "localhost:5000", + repository: ImageReference.Repository("swift"), + reference: "latest" + ) ) #expect( try! ImageReference(fromString: "swift", defaultRegistry: "localhost:5000") - == ImageReference(registry: "localhost:5000", repository: "swift", reference: "latest") + == ImageReference( + registry: "localhost:5000", + repository: ImageReference.Repository("swift"), + reference: "latest" + ) ) } } diff --git a/Tests/ContainerRegistryTests/SmokeTests.swift b/Tests/ContainerRegistryTests/SmokeTests.swift index d94c7a6..1754ead 100644 --- a/Tests/ContainerRegistryTests/SmokeTests.swift +++ b/Tests/ContainerRegistryTests/SmokeTests.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// import class Foundation.ProcessInfo -import ContainerRegistry +@testable import ContainerRegistry import Testing struct SmokeTests { @@ -31,7 +31,7 @@ struct SmokeTests { } @Test func testGetTags() async throws { - let repository = "testgettags" + let repository = try ImageReference.Repository("testgettags") // registry:2 does not validate the contents of the config or image blobs // so a smoke test can use simple data. Other registries are not so forgiving. @@ -73,7 +73,7 @@ struct SmokeTests { } @Test func testGetNonexistentBlob() async throws { - let repository = "testgetnonexistentblob" + let repository = try ImageReference.Repository("testgetnonexistentblob") do { let _ = try await client.getBlob( @@ -85,7 +85,7 @@ struct SmokeTests { } @Test func testCheckNonexistentBlob() async throws { - let repository = "testchecknonexistentblob" + let repository = try ImageReference.Repository("testchecknonexistentblob") let exists = try await client.blobExists( repository: repository, @@ -95,7 +95,7 @@ struct SmokeTests { } @Test func testPutAndGetBlob() async throws { - let repository = "testputandgetblob" // repository name must be lowercase + let repository = try ImageReference.Repository("testputandgetblob") let blob_data = "test".data(using: .utf8)! @@ -110,7 +110,7 @@ struct SmokeTests { } @Test func testPutAndGetTaggedManifest() async throws { - let repository = "testputandgettaggedmanifest" // repository name must be lowercase + let repository = try ImageReference.Repository("testputandgettaggedmanifest") // registry:2 does not validate the contents of the config or image blobs // so a smoke test can use simple data. Other registries are not so forgiving. @@ -145,7 +145,7 @@ struct SmokeTests { } @Test func testPutAndGetAnonymousManifest() async throws { - let repository = "testputandgetanonymousmanifest" // repository name must be lowercase + let repository = try ImageReference.Repository("testputandgetanonymousmanifest") // registry:2 does not validate the contents of the config or image blobs // so a smoke test can use simple data. Other registries are not so forgiving. @@ -184,7 +184,7 @@ struct SmokeTests { } @Test func testPutAndGetImageConfiguration() async throws { - let repository = "testputandgetimageconfiguration" // repository name must be lowercase + let repository = try ImageReference.Repository("testputandgetimageconfiguration") let image = ImageReference(registry: "registry", repository: repository, reference: "latest") let configuration = ImageConfiguration(