feat: implement version sub command (#911)

- closes #383
- implement version sub command, give more info

---------

Co-authored-by: fatelei <fatelei@fateleis-MacBook-Pro.local>
This commit is contained in:
wangxiaolei
2025-12-09 23:04:40 -03:00
committed by GitHub
co-authored by fatelei
parent 0cde1efe30
commit a2901e0517
11 changed files with 294 additions and 18 deletions
+12 -12
View File
@@ -78,8 +78,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms.git",
"state" : {
"revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b",
"version" : "1.0.4"
"revision" : "6c050d5ef8e1aa6342528460db614e9770d7f804",
"version" : "1.1.1"
}
},
{
@@ -96,8 +96,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-certificates.git",
"state" : {
"revision" : "c059d9c9d08d6654b9a92dda93d9049a278964c6",
"version" : "1.12.0"
"revision" : "133a347911b6ad0fc8fe3bf46ca90c66cff97130",
"version" : "1.17.0"
}
},
{
@@ -141,8 +141,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-structured-headers.git",
"state" : {
"revision" : "1625f271afb04375bf48737a5572613248d0e7a0",
"version" : "1.4.0"
"revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b",
"version" : "1.6.0"
}
},
{
@@ -150,8 +150,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-types.git",
"state" : {
"revision" : "a0a57e949a8903563aba4615869310c0ebf14c03",
"version" : "1.4.0"
"revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca",
"version" : "1.5.1"
}
},
{
@@ -195,8 +195,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "385f5bd783ffbfff46b246a7db7be8e4f04c53bd",
"version" : "2.33.0"
"revision" : "173cc69a058623525a58ae6710e2f5727c663793",
"version" : "2.36.0"
}
},
{
@@ -213,8 +213,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-numerics.git",
"state" : {
"revision" : "e0ec0f5f3af6f3e4d5e7a19d2af26b481acb6ba8",
"version" : "1.0.3"
"revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2",
"version" : "1.1.1"
}
},
{
@@ -43,6 +43,19 @@ extension ClientHealthCheck {
guard let apiServerCommit = reply.string(key: .apiServerCommit) else {
throw ContainerizationError(.internalError, message: "failed to decode apiServerCommit in health check")
}
return .init(appRoot: appRoot, installRoot: installRoot, apiServerVersion: apiServerVersion, apiServerCommit: apiServerCommit)
guard let apiServerBuild = reply.string(key: .apiServerBuild) else {
throw ContainerizationError(.internalError, message: "failed to decode apiServerBuild in health check")
}
guard let apiServerAppName = reply.string(key: .apiServerAppName) else {
throw ContainerizationError(.internalError, message: "failed to decode apiServerAppName in health check")
}
return .init(
appRoot: appRoot,
installRoot: installRoot,
apiServerVersion: apiServerVersion,
apiServerCommit: apiServerCommit,
apiServerBuild: apiServerBuild,
apiServerAppName: apiServerAppName
)
}
}
@@ -29,4 +29,10 @@ public struct SystemHealth: Sendable, Codable {
/// The Git commit ID for the container services.
public let apiServerCommit: String
/// The build type of the API server (debug|release).
public let apiServerBuild: String
/// The app name label returned by the server.
public let apiServerAppName: String
}
+2
View File
@@ -63,6 +63,8 @@ public enum XPCKeys: String {
case installRoot
case apiServerVersion
case apiServerCommit
case apiServerBuild
case apiServerAppName
/// Process request keys.
case signal
@@ -31,6 +31,7 @@ extension Application {
SystemStart.self,
SystemStatus.self,
SystemStop.self,
SystemVersion.self,
],
aliases: ["s"]
)
@@ -0,0 +1,89 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 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 ArgumentParser
import ContainerClient
import ContainerVersion
import Foundation
extension Application {
public struct SystemVersion: AsyncParsableCommand {
public static let configuration = CommandConfiguration(
commandName: "version",
abstract: "Show version information"
)
@Option(name: .long, help: "Format of the output")
var format: ListFormat = .table
@OptionGroup
var global: Flags.Global
public init() {}
public func run() async throws {
let cliInfo = VersionInfo(
version: ReleaseVersion.version(),
buildType: ReleaseVersion.buildType(),
commit: ReleaseVersion.gitCommit() ?? "unspecified",
appName: "container"
)
// Try to get API server version info
let serverInfo: VersionInfo?
do {
let health = try await ClientHealthCheck.ping(timeout: .seconds(2))
serverInfo = VersionInfo(
version: health.apiServerVersion,
buildType: health.apiServerBuild,
commit: health.apiServerCommit,
appName: health.apiServerAppName
)
} catch {
serverInfo = nil
}
let versions = [cliInfo, serverInfo].compactMap { $0 }
switch format {
case .table:
printVersionTable(versions: versions)
case .json:
try printVersionJSON(versions: versions)
}
}
private func printVersionTable(versions: [VersionInfo]) {
let header = ["COMPONENT", "VERSION", "BUILD", "COMMIT"]
let rows = [header] + versions.map { [$0.appName, $0.version, $0.buildType, $0.commit] }
let table = TableOutput(rows: rows)
print(table.format())
}
private func printVersionJSON(versions: [VersionInfo]) throws {
let data = try JSONEncoder().encode(versions)
print(String(data: data, encoding: .utf8) ?? "[]")
}
}
public struct VersionInfo: Codable {
let version: String
let buildType: String
let commit: String
let appName: String
}
}
@@ -19,16 +19,21 @@ import Foundation
public struct ReleaseVersion {
public static func singleLine(appName: String) -> String {
var versionDetails: [String: String] = ["build": "release"]
#if DEBUG
versionDetails["build"] = "debug"
#endif
var versionDetails: [String: String] = ["build": buildType()]
versionDetails["commit"] = gitCommit().map { String($0.prefix(7)) } ?? "unspecified"
let extras: String = versionDetails.map { "\($0): \($1)" }.sorted().joined(separator: ", ")
return "\(appName) version \(version()) (\(extras))"
}
public static func buildType() -> String {
#if DEBUG
return "debug"
#else
return "release"
#endif
}
public static func version() -> String {
let appBundle = Bundle.appBundle(executableURL: CommandLine.executablePathUrl)
let bundleVersion = appBundle?.infoDictionary?["CFBundleShortVersionString"] as? String
@@ -40,6 +40,9 @@ public actor HealthCheckHarness {
reply.set(key: .installRoot, value: installRoot.absoluteString)
reply.set(key: .apiServerVersion, value: ReleaseVersion.singleLine(appName: "container-apiserver"))
reply.set(key: .apiServerCommit, value: get_git_commit().map { String(cString: $0) } ?? "unspecified")
// Extra optional fields for richer client display
reply.set(key: .apiServerBuild, value: ReleaseVersion.buildType())
reply.set(key: .apiServerAppName, value: "container API Server")
return reply
}
}
@@ -0,0 +1,102 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 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 Foundation
import Testing
/// Tests for `container system version` output formats and build type detection.
final class TestCLIVersion: CLITest {
struct VersionInfo: Codable {
let version: String
let buildType: String
let commit: String
let appName: String
}
struct VersionJSON: Codable {
let version: String
let buildType: String
let commit: String
let appName: String
let server: VersionInfo?
}
private func expectedBuildType() throws -> String {
let path = try executablePath
if path.path.contains("/debug/") {
return "debug"
} else if path.path.contains("/release/") {
return "release"
}
// Fallback: prefer debug when ambiguous (matches SwiftPM default for tests)
return "debug"
}
@Test func defaultDisplaysTable() throws {
let (data, out, err, status) = try run(arguments: ["system", "version"]) // default is table
#expect(status == 0, "system version should succeed, stderr: \(err)")
#expect(!out.isEmpty)
// Validate table structure
let lines = out.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: .newlines)
#expect(lines.count >= 2) // header + at least CLI row
#expect(lines[0].contains("COMPONENT") && lines[0].contains("VERSION") && lines[0].contains("BUILD") && lines[0].contains("COMMIT"))
#expect(lines[1].hasPrefix("CLI "))
// Build should reflect the binary we are running (debug/release)
let expected = try expectedBuildType()
#expect(lines.joined(separator: "\n").contains(" CLI "))
#expect(lines.joined(separator: "\n").contains(" \(expected) "))
_ = data // silence unused warning if assertions short-circuit
}
@Test func jsonFormat() throws {
let (data, out, err, status) = try run(arguments: ["system", "version", "--format", "json"])
#expect(status == 0, "system version --format json should succeed, stderr: \(err)")
#expect(!out.isEmpty)
let decoded = try JSONDecoder().decode(VersionJSON.self, from: data)
#expect(decoded.appName == "container CLI")
#expect(!decoded.version.isEmpty)
#expect(!decoded.commit.isEmpty)
let expected = try expectedBuildType()
#expect(decoded.buildType == expected)
}
@Test func explicitTableFormat() throws {
let (_, out, err, status) = try run(arguments: ["system", "version", "--format", "table"])
#expect(status == 0, "system version --format table should succeed, stderr: \(err)")
#expect(!out.isEmpty)
let lines = out.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: .newlines)
#expect(lines.count >= 2)
#expect(lines[0].contains("COMPONENT") && lines[0].contains("VERSION") && lines[0].contains("BUILD") && lines[0].contains("COMMIT"))
#expect(lines[1].hasPrefix("CLI "))
}
@Test func buildTypeMatchesBinary() throws {
// Validate build type via JSON to avoid parsing table text loosely
let (data, _, err, status) = try run(arguments: ["system", "version", "--format", "json"])
#expect(status == 0, "version --format json should succeed, stderr: \(err)")
let decoded = try JSONDecoder().decode(VersionJSON.self, from: data)
let expected = try expectedBuildType()
#expect(decoded.buildType == expected, "Expected build type \(expected) but got \(decoded.buildType)")
}
}
+49
View File
@@ -925,6 +925,55 @@ container system status [--prefix <prefix>] [--debug]
* `-p, --prefix <prefix>`: Launchd prefix for services (default: com.apple.container.)
### `container system version`
Shows version information for the CLI and, if available, the API server. The table format is consistent with other list outputs and includes a header. If the API server responds to a health check, a second row for the server is added.
**Usage**
```bash
container system version [--format <format>]
```
**Options**
* `--format <format>`: Output format (values: json, table; default: table)
**Table Output**
Columns: `COMPONENT`, `VERSION`, `BUILD`, `COMMIT`.
Example:
```bash
container system version
```
```
COMPONENT VERSION BUILD COMMIT
CLI 1.2.3 debug abcdef1
API Server container-apiserver 1.2.3 release 1234abc
```
**JSON Output**
Backward-compatible with previous CLI-only output. Top-level fields describe the CLI. When available, a `server` object is included with the same fields.
```json
{
"version": "1.2.3",
"buildType": "debug",
"commit": "abcdef1",
"appName": "container CLI",
"server": {
"version": "container-apiserver 1.2.3",
"buildType": "release",
"commit": "1234abc",
"appName": "container API Server"
}
}
```
### `container system logs`
Displays logs from the container services. You can specify a time interval or follow new logs in real time.
+7 -1
View File
@@ -58,9 +58,15 @@ USAGE: container [--debug] <subcommand>
OPTIONS:
--debug Enable debug output [environment: CONTAINER_DEBUG]
--version Show the version.
--version Show the CLI version (single line).
-h, --help Show help information.
Detailed version information is available under the system command:
```
container system version [--format json|table]
```
CONTAINER SUBCOMMANDS:
create Create a new container
delete, rm Delete one or more containers