Skip to content

Commit c9f81ca

Browse files
authored
Feat: add container registry list (#1119)
- Requires apple/containerization#502 - Closes #1088 --------- Signed-off-by: ChengHao Yang <17496418+tico88612@users.noreply.github.com>
1 parent cf9b335 commit c9f81ca

7 files changed

Lines changed: 166 additions & 7 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ integration: init-block
188188
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIRunCommand2 || exit_code=1 ; \
189189
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIRunCommand3 || exit_code=1 ; \
190190
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIPruneCommand || exit_code=1 ; \
191+
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIRegistry || exit_code=1 ; \
191192
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIStatsCommand || exit_code=1 ; \
192193
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIImagesCommand || exit_code=1 ; \
193194
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIRunBase || exit_code=1 ; \

Sources/ContainerCommands/Registry/RegistryCommand.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ extension Application {
2323
commandName: "registry",
2424
abstract: "Manage registry logins",
2525
subcommands: [
26-
Login.self,
27-
Logout.self,
26+
RegistryLogin.self,
27+
RegistryLogout.self,
28+
RegistryList.self,
2829
],
2930
aliases: ["r"]
3031
)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import ArgumentParser
18+
import ContainerAPIClient
19+
import ContainerizationOCI
20+
import ContainerizationOS
21+
import Foundation
22+
23+
extension Application {
24+
public struct RegistryList: AsyncLoggableCommand {
25+
@OptionGroup
26+
public var logOptions: Flags.Logging
27+
28+
@Option(name: .long, help: "Format of the output")
29+
var format: ListFormat = .table
30+
31+
@Flag(name: .shortAndLong, help: "Only output the registry name")
32+
var quiet = false
33+
34+
public init() {}
35+
public static let configuration = CommandConfiguration(
36+
commandName: "list",
37+
abstract: "List image registry logins",
38+
aliases: ["ls"])
39+
40+
public func run() async throws {
41+
let keychain = KeychainHelper(securityDomain: Constants.keychainID)
42+
let registries = try keychain.list()
43+
try printRegistries(registries: registries, format: format)
44+
}
45+
46+
private func createHeader() -> [[String]] {
47+
[["HOSTNAME", "USERNAME", "MODIFIED", "CREATED"]]
48+
}
49+
50+
private func printRegistries(registries: [RegistryInfo], format: ListFormat) throws {
51+
if format == .json {
52+
let printables = registries.map {
53+
PrintableRegistry($0)
54+
}
55+
let data = try JSONEncoder().encode(printables)
56+
print(String(decoding: data, as: UTF8.self))
57+
58+
return
59+
}
60+
61+
if self.quiet {
62+
registries.forEach {
63+
print($0.hostname)
64+
}
65+
return
66+
}
67+
68+
var rows = createHeader()
69+
for registry in registries {
70+
rows.append(registry.asRow)
71+
}
72+
73+
let formatter = TableOutput(rows: rows)
74+
print(formatter.format())
75+
}
76+
}
77+
}
78+
extension RegistryInfo {
79+
fileprivate var asRow: [String] {
80+
[
81+
self.hostname,
82+
self.username,
83+
self.modifiedDate.ISO8601Format(),
84+
self.createdDate.ISO8601Format(),
85+
]
86+
}
87+
}
88+
struct PrintableRegistry: Codable {
89+
let hostname: String
90+
let username: String
91+
let modifiedDate: Date
92+
let createdDate: Date
93+
94+
init(_ registry: RegistryInfo) {
95+
self.hostname = registry.hostname
96+
self.username = registry.username
97+
self.modifiedDate = registry.modifiedDate
98+
self.createdDate = registry.createdDate
99+
}
100+
}

Sources/ContainerCommands/Registry/Login.swift renamed to Sources/ContainerCommands/Registry/RegistryLogin.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//===----------------------------------------------------------------------===//
2-
// Copyright © 2025-2026 Apple Inc. and the container project authors.
2+
// Copyright © 2026 Apple Inc. and the container project authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -22,9 +22,10 @@ import ContainerizationOCI
2222
import Foundation
2323

2424
extension Application {
25-
public struct Login: AsyncLoggableCommand {
25+
public struct RegistryLogin: AsyncLoggableCommand {
2626
public init() {}
2727
public static let configuration = CommandConfiguration(
28+
commandName: "login",
2829
abstract: "Log in to a registry"
2930
)
3031

Sources/ContainerCommands/Registry/Logout.swift renamed to Sources/ContainerCommands/Registry/RegistryLogout.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//===----------------------------------------------------------------------===//
2-
// Copyright © 2025-2026 Apple Inc. and the container project authors.
2+
// Copyright © 2026 Apple Inc. and the container project authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -20,10 +20,12 @@ import Containerization
2020
import ContainerizationOCI
2121

2222
extension Application {
23-
public struct Logout: AsyncLoggableCommand {
23+
public struct RegistryLogout: AsyncLoggableCommand {
2424
public init() {}
2525
public static let configuration = CommandConfiguration(
26-
abstract: "Log out from a registry")
26+
commandName: "logout",
27+
abstract: "Log out from a registry"
28+
)
2729

2830
@OptionGroup
2931
public var logOptions: Flags.Logging
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
import Testing
19+
20+
class TestCLIRegistry: CLITest {
21+
@Test func testListDefaultFormat() throws {
22+
let (_, output, error, status) = try run(arguments: ["registry", "list"])
23+
#expect(status == 0, "registry list should succeed, stderr: \(error)")
24+
25+
// Check for table header
26+
let requiredHeaders = ["HOSTNAME", "USERNAME", "MODIFIED", "CREATED"]
27+
#expect(
28+
requiredHeaders.allSatisfy { output.contains($0) },
29+
"output should contain all required headers"
30+
)
31+
}
32+
33+
@Test func testListQuietMode() throws {
34+
let (_, output, error, status) = try run(arguments: ["registry", "list", "-q"])
35+
#expect(status == 0, "registry list -q should succeed, stderr: \(error)")
36+
37+
#expect(!output.contains("HOSTNAME"), "quiet mode should not contain headers")
38+
}
39+
}

docs/command-reference.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,21 @@ container registry logout [--debug] <registry>
919919

920920
No options.
921921

922+
### `container registry list`
923+
924+
List image registry logins.
925+
926+
**Usage**
927+
928+
```bash
929+
container registry list [--format <format>] [--quiet] [--debug]
930+
```
931+
932+
**Options**
933+
934+
* `--format <format>`: Format of the output (values: json, table; default: table)
935+
* `-q, --quiet`: Only output the image registry name
936+
922937
## System Management
923938

924939
System commands manage the container apiserver, logs, DNS settings and kernel. These are only available on macOS hosts.

0 commit comments

Comments
 (0)