mirror of
https://github.com/apple/container.git
synced 2026-09-26 01:25:52 +00:00
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:
+12
-12
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user