diff --git a/Sources/ContainerCommands/Registry/RegistryList.swift b/Sources/ContainerCommands/Registry/RegistryList.swift index 2201f2429..fbde5519d 100644 --- a/Sources/ContainerCommands/Registry/RegistryList.swift +++ b/Sources/ContainerCommands/Registry/RegistryList.swift @@ -39,10 +39,8 @@ extension Application { aliases: ["ls"]) public func run() async throws { - let keychain = KeychainHelper(securityDomain: Constants.keychainID) - let registryInfos = try keychain.list() - let registries = registryInfos.map { RegistryResource(from: $0) } - + let client = RegistryKeychainClient() + let registries = try await client.list() try printRegistries(registries: registries, format: format) } diff --git a/Sources/ContainerCommands/Registry/RegistryLogin.swift b/Sources/ContainerCommands/Registry/RegistryLogin.swift index 96d5919f5..8969596dd 100644 --- a/Sources/ContainerCommands/Registry/RegistryLogin.swift +++ b/Sources/ContainerCommands/Registry/RegistryLogin.swift @@ -16,6 +16,7 @@ import ArgumentParser import ContainerAPIClient +import ContainerResource import Containerization import ContainerizationError import ContainerizationOCI @@ -45,6 +46,7 @@ extension Application { var server: String public func run() async throws { + let keychainClient = RegistryKeychainClient() var username = self.username var password = "" if passwordStdin { @@ -57,12 +59,11 @@ extension Application { } password = String(decoding: passwordData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) } - let keychain = KeychainHelper(securityDomain: Constants.keychainID) if username == "" { - username = try keychain.userPrompt(hostname: server) + username = try await keychainClient.userPrompt(hostname: server) } if password == "" { - password = try keychain.passwordPrompt() + password = try await keychainClient.passwordPrompt() print() } @@ -90,7 +91,7 @@ extension Application { ) ) try await client.ping() - try keychain.save(hostname: server, username: username, password: password) + try await keychainClient.login(hostname: server, username: username, password: password) print("Login succeeded") } } diff --git a/Sources/ContainerCommands/Registry/RegistryLogout.swift b/Sources/ContainerCommands/Registry/RegistryLogout.swift index 2f5a2432b..703b794a8 100644 --- a/Sources/ContainerCommands/Registry/RegistryLogout.swift +++ b/Sources/ContainerCommands/Registry/RegistryLogout.swift @@ -34,9 +34,9 @@ extension Application { var registry: String public func run() async throws { - let keychain = KeychainHelper(securityDomain: Constants.keychainID) - let r = Reference.resolveDomain(domain: registry) - try keychain.delete(hostname: r) + let hostname = Reference.resolveDomain(domain: registry) + let client = RegistryKeychainClient() + try await client.logout(hostname: hostname) } } } diff --git a/Sources/Services/ContainerAPIService/Client/RegistryKeychainClient.swift b/Sources/Services/ContainerAPIService/Client/RegistryKeychainClient.swift new file mode 100644 index 000000000..62e80d93b --- /dev/null +++ b/Sources/Services/ContainerAPIService/Client/RegistryKeychainClient.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the container project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ContainerResource +import ContainerXPC +import ContainerizationOCI +import ContainerizationOS +import Foundation + +/// A client for managing registry credentials. +public struct RegistryKeychainClient { + private let keychain: KeychainHelper + + /// Creates a a new instance using the default registry keychain domain. + public init() { + self.keychain = KeychainHelper(securityDomain: Constants.keychainID) + } + + /// Returns all registry credentials stored in the keychain. + /// - Returns: An array of `RegistryResource` values representing saved registry entries. + /// - Throws: An error if the keychain query fails. + public func list() async throws -> [RegistryResource] { + let registries = try keychain.list() + return registries.map { RegistryResource(from: $0) } + } + + /// Stores credentials for a registry in the keychain. + /// - Parameters: + /// - hostname: The registry hostname. + /// - username: The username for authentication. + /// - password: The password for authentication. + /// - Throws: An error if the credentials cannot be saved to the keychain. + public func login(hostname: String, username: String, password: String) async throws { + try keychain.save(hostname: hostname, username: username, password: password) + } + + /// Removes stored credentials for a registry from the keychain. + /// - Parameter hostname: The registry hostname. + /// - Throws: An error if the credentials cannot be removed. + public func logout(hostname: String) async throws { + try keychain.delete(hostname: hostname) + } + + /// Prompts the user to enter a registry username. + /// - Parameter hostname: The registry hostname being authenticated. + /// - Returns: The username entered by the user. + /// - Throws: An error if input cannot be read. + public func userPrompt(hostname: String) async throws -> String { + try keychain.userPrompt(hostname: hostname) + } + + /// Prompts the user to enter a registry password. + /// - Returns: The password entered by the user. + /// - Throws: An error if input cannot be read. + public func passwordPrompt() async throws -> String { + try keychain.passwordPrompt() + } +}