chore: import upstream snapshot with attribution
container project - merge build / Invoke build (push) Successful in 1s
container project - merge build / Invoke build (push) Successful in 1s
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerPersistence
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import DNSServer
|
||||
import Foundation
|
||||
|
||||
extension Application {
|
||||
public struct DNSCreate: AsyncLoggableCommand {
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "create",
|
||||
abstract: "Create a local DNS domain for containers (must run as an administrator)"
|
||||
)
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
@Option(name: .long, help: "Set the ip address to be redirected to localhost")
|
||||
var localhost: String?
|
||||
|
||||
@Argument(help: "The local domain name")
|
||||
var domainName: String
|
||||
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
var localhostIP: IPAddress? = nil
|
||||
if let localhost {
|
||||
localhostIP = try? IPAddress(localhost)
|
||||
guard let localhostIP, case .v4(_) = localhostIP else {
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid IPv4 address: \(localhost)")
|
||||
}
|
||||
}
|
||||
|
||||
guard let domainName = try? DNSName(domainName) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid domain name: \(domainName)")
|
||||
}
|
||||
|
||||
let resolver: HostDNSResolver = HostDNSResolver()
|
||||
do {
|
||||
try resolver.createDomain(name: domainName, localhost: localhostIP)
|
||||
} catch let error as ContainerizationError {
|
||||
throw error
|
||||
} catch {
|
||||
throw ContainerizationError(.invalidState, message: "cannot create domain (try sudo?)")
|
||||
}
|
||||
|
||||
let pf = PacketFilter()
|
||||
if let from = localhostIP {
|
||||
let to = try! IPAddress("127.0.0.1")
|
||||
do {
|
||||
try pf.createRedirectRule(from: from, to: to, domain: domainName)
|
||||
} catch {
|
||||
_ = try resolver.deleteDomain(name: domainName)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
print(domainName.pqdn)
|
||||
|
||||
if localhostIP != nil {
|
||||
do {
|
||||
try pf.reinitialize()
|
||||
} catch let error as ContainerizationError {
|
||||
throw error
|
||||
} catch {
|
||||
throw ContainerizationError(.invalidState, message: "failed loading pf rules")
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try HostDNSResolver.reinitialize()
|
||||
} catch {
|
||||
throw ContainerizationError(.invalidState, message: "mDNSResponder restart failed, run `sudo killall -HUP mDNSResponder` to deactivate domain")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import DNSServer
|
||||
import Foundation
|
||||
|
||||
extension Application {
|
||||
public struct DNSDelete: AsyncLoggableCommand {
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "delete",
|
||||
abstract: "Delete a local DNS domain (must run as an administrator)",
|
||||
aliases: ["rm"]
|
||||
)
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
@Argument(help: "The local domain name")
|
||||
var domainName: String
|
||||
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
guard let domainName = try? DNSName(domainName) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid domain name: \(domainName)")
|
||||
}
|
||||
|
||||
let resolver = HostDNSResolver()
|
||||
var localhostIP: IPAddress?
|
||||
do {
|
||||
localhostIP = try resolver.deleteDomain(name: domainName)
|
||||
} catch {
|
||||
throw ContainerizationError(.invalidState, message: "cannot delete domain (try sudo?)")
|
||||
}
|
||||
|
||||
do {
|
||||
try HostDNSResolver.reinitialize()
|
||||
} catch {
|
||||
throw ContainerizationError(.invalidState, message: "mDNSResponder restart failed, run `sudo killall -HUP mDNSResponder` to deactivate domain")
|
||||
}
|
||||
|
||||
guard let localhostIP else {
|
||||
print(domainName.pqdn)
|
||||
return
|
||||
}
|
||||
|
||||
let pf = PacketFilter()
|
||||
try pf.removeRedirectRule(from: localhostIP, to: try! IPAddress("127.0.0.1"), domain: domainName)
|
||||
|
||||
do {
|
||||
try pf.reinitialize()
|
||||
} catch let error as ContainerizationError {
|
||||
throw error
|
||||
} catch {
|
||||
throw ContainerizationError(.invalidState, message: "failed loading pf rules")
|
||||
}
|
||||
print(domainName.pqdn)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import DNSServer
|
||||
import Foundation
|
||||
|
||||
extension Application {
|
||||
public struct DNSList: AsyncLoggableCommand {
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "list",
|
||||
abstract: "List local DNS domains",
|
||||
aliases: ["ls"]
|
||||
)
|
||||
|
||||
@Option(name: .long, help: "Format of the output")
|
||||
var format: ListFormat = .table
|
||||
|
||||
@Flag(name: .shortAndLong, help: "Only output the domain")
|
||||
var quiet = false
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
let resolver = HostDNSResolver()
|
||||
let domains = resolver.listDomains()
|
||||
|
||||
try Output.render(
|
||||
payload: domains.map { $0.pqdn },
|
||||
display: domains.map { PrintableDomain($0) },
|
||||
format: format, quiet: quiet
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct PrintableDomain: ListDisplayable {
|
||||
let domain: DNSName
|
||||
|
||||
init(_ domain: DNSName) {
|
||||
self.domain = domain
|
||||
}
|
||||
|
||||
static var tableHeader: [String] {
|
||||
["DOMAIN"]
|
||||
}
|
||||
|
||||
var tableRow: [String] {
|
||||
[domain.pqdn]
|
||||
}
|
||||
|
||||
var quietValue: String {
|
||||
domain.pqdn
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerPersistence
|
||||
import ContainerPlugin
|
||||
import Containerization
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import ContainerizationOCI
|
||||
import Foundation
|
||||
import TerminalProgress
|
||||
|
||||
extension Application {
|
||||
public struct KernelSet: AsyncLoggableCommand {
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "set",
|
||||
abstract: "Set the default kernel"
|
||||
)
|
||||
|
||||
@Option(name: .long, help: "The architecture of the kernel binary (values: amd64, arm64)")
|
||||
var arch: String = ContainerizationOCI.Platform.current.architecture.description
|
||||
|
||||
@Option(name: .customLong("binary"), help: "Path to the kernel file (or archive member, if used with --tar)")
|
||||
var binaryPath: String? = nil
|
||||
|
||||
@Flag(name: .long, help: "Overwrites an existing kernel with the same name")
|
||||
var force: Bool = false
|
||||
|
||||
@Flag(name: .long, help: "Download and install the recommended kernel as the default (takes precedence over all other flags)")
|
||||
var recommended: Bool = false
|
||||
|
||||
@Option(name: .customLong("tar"), help: "Filesystem path or remote URL to a tar archive containing a kernel file")
|
||||
var tarPath: String? = nil
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try await Application.loadContainerSystemConfig()
|
||||
if recommended {
|
||||
let url = containerSystemConfig.kernel.url
|
||||
let path: String = containerSystemConfig.kernel.binaryPath
|
||||
log.info("Installing the recommended kernel from \(url)...")
|
||||
try await Self.downloadAndInstallWithProgressBar(tarRemoteURL: url, kernelFilePath: path, force: force)
|
||||
return
|
||||
}
|
||||
guard tarPath != nil else {
|
||||
return try await self.setKernelFromBinary()
|
||||
}
|
||||
try await self.setKernelFromTar()
|
||||
}
|
||||
|
||||
private func setKernelFromBinary() async throws {
|
||||
guard let binaryPath else {
|
||||
throw ArgumentParser.ValidationError("missing argument '--binary'")
|
||||
}
|
||||
let absolutePath = URL(fileURLWithPath: binaryPath, relativeTo: .currentDirectory()).absoluteURL.absoluteString
|
||||
let platform = try getSystemPlatform()
|
||||
try await ClientKernel.installKernel(kernelFilePath: absolutePath, platform: platform, force: force)
|
||||
}
|
||||
|
||||
private func setKernelFromTar() async throws {
|
||||
guard let binaryPath else {
|
||||
throw ArgumentParser.ValidationError("missing argument '--binary'")
|
||||
}
|
||||
guard let tarPath else {
|
||||
throw ArgumentParser.ValidationError("missing argument '--tar")
|
||||
}
|
||||
let platform = try getSystemPlatform()
|
||||
let localTarPath = URL(fileURLWithPath: tarPath, relativeTo: .currentDirectory()).path
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: localTarPath) {
|
||||
try await ClientKernel.installKernelFromTar(tarFile: localTarPath, kernelFilePath: binaryPath, platform: platform, force: force)
|
||||
return
|
||||
}
|
||||
guard let remoteURL = URL(string: tarPath) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid remote URL '\(tarPath)' for argument '--tar'. Missing protocol?")
|
||||
}
|
||||
try await Self.downloadAndInstallWithProgressBar(tarRemoteURL: remoteURL, kernelFilePath: binaryPath, platform: platform, force: force)
|
||||
}
|
||||
|
||||
private func getSystemPlatform() throws -> SystemPlatform {
|
||||
switch arch {
|
||||
case "arm64":
|
||||
return .linuxArm
|
||||
case "amd64":
|
||||
return .linuxAmd
|
||||
default:
|
||||
throw ContainerizationError(.unsupported, message: "unsupported architecture \(arch)")
|
||||
}
|
||||
}
|
||||
|
||||
static func downloadAndInstallWithProgressBar(tarRemoteURL: URL, kernelFilePath: String, platform: SystemPlatform = .current, force: Bool) async throws {
|
||||
let progressConfig = try ProgressConfig(
|
||||
showTasks: true,
|
||||
totalTasks: 2
|
||||
)
|
||||
let progress = ProgressBar(config: progressConfig)
|
||||
defer {
|
||||
progress.finish()
|
||||
}
|
||||
progress.start()
|
||||
try await ClientKernel.installKernelFromTar(
|
||||
tarFile: tarRemoteURL.absoluteString, kernelFilePath: kernelFilePath, platform: platform, progressUpdate: progress.handler, force: force)
|
||||
progress.finish()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerPersistence
|
||||
import ContainerPlugin
|
||||
import Foundation
|
||||
|
||||
enum ListOutputFormat: String, Decodable, ExpressibleByArgument {
|
||||
case json
|
||||
case toml
|
||||
}
|
||||
|
||||
extension Application {
|
||||
public struct PropertyList: AsyncLoggableCommand {
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "list",
|
||||
abstract: "List system properties",
|
||||
aliases: ["ls"]
|
||||
)
|
||||
|
||||
@Option(name: .long, help: "Format of the output")
|
||||
var format: ListOutputFormat = .toml
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try await Application.loadContainerSystemConfig()
|
||||
let output =
|
||||
switch format {
|
||||
case .json: try Output.renderJSON(containerSystemConfig)
|
||||
case .toml: try Output.renderTOML(containerSystemConfig)
|
||||
}
|
||||
Output.emit(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
|
||||
extension Application {
|
||||
public struct SystemCommand: AsyncLoggableCommand {
|
||||
public init() {}
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "system",
|
||||
abstract: "Manage system components",
|
||||
subcommands: [
|
||||
SystemDF.self,
|
||||
SystemDNS.self,
|
||||
SystemKernel.self,
|
||||
SystemLogs.self,
|
||||
SystemProperty.self,
|
||||
SystemStart.self,
|
||||
SystemStatus.self,
|
||||
SystemStop.self,
|
||||
SystemVersion.self,
|
||||
],
|
||||
aliases: ["s"]
|
||||
)
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import Foundation
|
||||
|
||||
extension Application {
|
||||
public struct SystemDF: AsyncLoggableCommand {
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "df",
|
||||
abstract: "Show disk usage for images, containers, and volumes"
|
||||
)
|
||||
|
||||
@Option(name: .long, help: "Format of the output")
|
||||
var format: ListFormat = .table
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
let stats = try await ClientDiskUsage.get()
|
||||
try Output.render(payload: stats, format: format, jsonOptions: .pretty) {
|
||||
diskUsageTable(stats: stats)
|
||||
}
|
||||
}
|
||||
|
||||
private func diskUsageTable(stats: DiskUsageStats) -> String {
|
||||
var rows: [[String]] = []
|
||||
|
||||
// Header row
|
||||
rows.append(["TYPE", "TOTAL", "ACTIVE", "SIZE", "RECLAIMABLE"])
|
||||
|
||||
// Images row
|
||||
rows.append([
|
||||
"Images",
|
||||
"\(stats.images.total)",
|
||||
"\(stats.images.active)",
|
||||
formatSize(stats.images.sizeInBytes),
|
||||
formatReclaimable(stats.images.reclaimable, total: stats.images.sizeInBytes),
|
||||
])
|
||||
|
||||
// Containers row
|
||||
rows.append([
|
||||
"Containers",
|
||||
"\(stats.containers.total)",
|
||||
"\(stats.containers.active)",
|
||||
formatSize(stats.containers.sizeInBytes),
|
||||
formatReclaimable(stats.containers.reclaimable, total: stats.containers.sizeInBytes),
|
||||
])
|
||||
|
||||
// Volumes row
|
||||
rows.append([
|
||||
"Local Volumes",
|
||||
"\(stats.volumes.total)",
|
||||
"\(stats.volumes.active)",
|
||||
formatSize(stats.volumes.sizeInBytes),
|
||||
formatReclaimable(stats.volumes.reclaimable, total: stats.volumes.sizeInBytes),
|
||||
])
|
||||
|
||||
let tableFormatter = TableOutput(rows: rows)
|
||||
return tableFormatter.format()
|
||||
}
|
||||
|
||||
private func formatSize(_ bytes: UInt64) -> String {
|
||||
if bytes == 0 {
|
||||
return "0 B"
|
||||
}
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.countStyle = .file
|
||||
return formatter.string(fromByteCount: Int64(bytes))
|
||||
}
|
||||
|
||||
private func formatReclaimable(_ reclaimable: UInt64, total: UInt64) -> String {
|
||||
let sizeStr = formatSize(reclaimable)
|
||||
|
||||
if total == 0 {
|
||||
return "\(sizeStr) (0%)"
|
||||
}
|
||||
|
||||
// Cap at 100% in case reclaimable > total (shouldn't happen but be defensive)
|
||||
let percentage = min(100, Int(round(Double(reclaimable) / Double(total) * 100.0)))
|
||||
return "\(sizeStr) (\(percentage)%)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerizationError
|
||||
import Foundation
|
||||
|
||||
extension Application {
|
||||
public struct SystemDNS: AsyncLoggableCommand {
|
||||
public init() {}
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "dns",
|
||||
abstract: "Manage local DNS domains",
|
||||
subcommands: [
|
||||
DNSCreate.self,
|
||||
DNSDelete.self,
|
||||
DNSList.self,
|
||||
]
|
||||
)
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
|
||||
extension Application {
|
||||
public struct SystemKernel: AsyncLoggableCommand {
|
||||
public init() {}
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "kernel",
|
||||
abstract: "Manage the default kernel configuration",
|
||||
subcommands: [
|
||||
KernelSet.self
|
||||
]
|
||||
)
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerizationError
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
extension Application {
|
||||
public struct SystemLogs: AsyncLoggableCommand {
|
||||
public static let subsystem = "com.apple.container"
|
||||
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "logs",
|
||||
abstract: "Fetch system logs for `container` services"
|
||||
)
|
||||
|
||||
@Flag(name: .shortAndLong, help: "Follow log output")
|
||||
var follow: Bool = false
|
||||
|
||||
@Option(
|
||||
name: .long,
|
||||
help: "Fetch logs starting from the specified time period (minus the current time); supported formats: <number>[m|h|d] (defaults to seconds)"
|
||||
)
|
||||
var last: String = "5m"
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
public init() {}
|
||||
|
||||
public func validate() throws {
|
||||
if follow { return }
|
||||
if let value = Int(last), value > 0 { return }
|
||||
guard let unit = last.last, "mhd".contains(unit),
|
||||
let value = Int(last.dropLast()), value > 0
|
||||
else {
|
||||
throw ContainerizationError(
|
||||
.invalidArgument,
|
||||
message: "invalid --last value '\(last)': expected a positive integer (seconds) or a value with suffix m, h, or d (e.g. 30, 5m, 1h, 2d)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func run() async throws {
|
||||
let process = Process()
|
||||
let sigHandler = AsyncSignalHandler.create(notify: [SIGINT, SIGTERM])
|
||||
|
||||
Task {
|
||||
for await _ in sigHandler.signals {
|
||||
process.terminate()
|
||||
Darwin.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
var args = ["log"]
|
||||
args.append(self.follow ? "stream" : "show")
|
||||
args.append(contentsOf: ["--info", logOptions.debug ? "--debug" : nil].compactMap { $0 })
|
||||
if !self.follow {
|
||||
args.append(contentsOf: ["--last", last])
|
||||
}
|
||||
args.append(contentsOf: ["--predicate", "subsystem = 'com.apple.container'"])
|
||||
|
||||
process.launchPath = "/usr/bin/env"
|
||||
process.arguments = args
|
||||
|
||||
process.standardOutput = FileHandle.standardOutput
|
||||
process.standardError = FileHandle.standardError
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.invalidArgument,
|
||||
message: "failed to system logs: \(error)"
|
||||
)
|
||||
}
|
||||
throw ArgumentParser.ExitCode(process.terminationStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerPersistence
|
||||
import ContainerizationError
|
||||
import Foundation
|
||||
|
||||
extension Application {
|
||||
public struct SystemProperty: AsyncLoggableCommand {
|
||||
public init() {}
|
||||
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "property",
|
||||
abstract: "Manage system property values",
|
||||
subcommands: [
|
||||
PropertyList.self
|
||||
]
|
||||
)
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerPersistence
|
||||
import ContainerPlugin
|
||||
import ContainerXPC
|
||||
import ContainerizationError
|
||||
import Foundation
|
||||
import MachineAPIClient
|
||||
import SystemPackage
|
||||
import TerminalProgress
|
||||
|
||||
extension Application {
|
||||
public struct SystemStart: AsyncLoggableCommand {
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "start",
|
||||
abstract: "Start `container` services"
|
||||
)
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Path to the root directory for application data",
|
||||
transform: { FilePath(FileManager.default.currentDirectoryPath).resolve($0, defaultPath: FilePath($0)) })
|
||||
var appRoot = ApplicationRoot.defaultPath
|
||||
|
||||
@Option(
|
||||
name: .long,
|
||||
help: "Path to the root directory for application executables and plugins",
|
||||
transform: { FilePath(FileManager.default.currentDirectoryPath).resolve($0, defaultPath: FilePath($0)) })
|
||||
var installRoot = InstallRoot.defaultPath
|
||||
|
||||
@Option(
|
||||
name: .long,
|
||||
help: "Path to the root directory for log data, using macOS log facility if not set",
|
||||
transform: { FilePath(FileManager.default.currentDirectoryPath).resolve($0, defaultPath: FilePath($0)) })
|
||||
var logRoot: FilePath? = nil
|
||||
|
||||
@Flag(
|
||||
name: .long,
|
||||
inversion: .prefixedEnableDisable,
|
||||
help: "Specify whether the default kernel should be installed or not (default: prompt user)")
|
||||
var kernelInstall: Bool?
|
||||
|
||||
@Option(
|
||||
help: "Number of seconds to wait for API service to become responsive",
|
||||
transform: {
|
||||
guard let timeoutSeconds = Double($0) else {
|
||||
throw ValidationError("Invalid timeout value: \($0)")
|
||||
}
|
||||
return .seconds(timeoutSeconds)
|
||||
}
|
||||
)
|
||||
var timeout: Duration = XPCClient.xpcRegistrationTimeout
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
try ConfigurationLoader.copyConfigurationToReadOnly(to: appRoot)
|
||||
// Pass appRoot before installRoot: ConfigurationLoader uses first-match-wins
|
||||
// precedence, so user-provided config in appRoot overrides the defaults
|
||||
// shipped under installRoot. Both layers are passed explicitly because
|
||||
// users can override --app-root and --install-root from the CLI, and the
|
||||
// loader's default search would otherwise ignore those overrides.
|
||||
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load(
|
||||
configurationFiles: [
|
||||
ConfigurationLoader.configurationFile(in: appRoot, of: .appRoot),
|
||||
ConfigurationLoader.configurationFile(in: installRoot, of: .installRoot),
|
||||
])
|
||||
|
||||
// Without the true path to the binary in the plist, `container-apiserver` won't launch properly.
|
||||
// Resolve the symlink to get the true binary path before writing the launchd plist.
|
||||
// Gatekeeper / amfid validates code signatures relative to the enclosing .app bundle
|
||||
// hierarchy; launching via a symlink outside the bundle fails that check.
|
||||
// TODO: Can we use the plugin loader to bootstrap the API server?
|
||||
let executablePath = try CommandLine.executablePath
|
||||
.removingLastComponent()
|
||||
.appending(FilePath.Component("container-apiserver"))
|
||||
.resolvingSymlinks()
|
||||
|
||||
var args = [executablePath.string]
|
||||
|
||||
args.append("start")
|
||||
if logOptions.debug {
|
||||
args.append("--debug")
|
||||
}
|
||||
|
||||
let apiServerDataPath = appRoot.appending(FilePath.Component("apiserver"))
|
||||
let apiServerDataURL = URL(fileURLWithPath: apiServerDataPath.string)
|
||||
try FileManager.default.createDirectory(at: apiServerDataURL, withIntermediateDirectories: true)
|
||||
|
||||
var env = PluginLoader.filterEnvironment()
|
||||
env[ApplicationRoot.environmentName] = appRoot.string
|
||||
env[InstallRoot.environmentName] = installRoot.string
|
||||
if let logRoot {
|
||||
env[LogRoot.environmentName] = logRoot.string
|
||||
}
|
||||
let plist = LaunchPlist(
|
||||
label: "com.apple.container.apiserver",
|
||||
arguments: args,
|
||||
environment: env,
|
||||
limitLoadToSessionType: [.Aqua, .Background, .System],
|
||||
runAtLoad: true,
|
||||
machServices: ["com.apple.container.apiserver"]
|
||||
)
|
||||
|
||||
let plistPath = apiServerDataPath.appending(FilePath.Component("apiserver.plist"))
|
||||
let plistURL = URL(fileURLWithPath: plistPath.string)
|
||||
let data = try plist.encode()
|
||||
try data.write(to: plistURL)
|
||||
|
||||
log.info("Launching container-apiserver...")
|
||||
try ServiceManager.register(plistPath: plistURL.path)
|
||||
|
||||
// Now ping our friendly daemon. Fail if we don't get a response.
|
||||
do {
|
||||
log.info("Testing access to container-apiserver...")
|
||||
_ = try await ClientHealthCheck.ping(timeout: timeout)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
message: "failed to get a response from apiserver: \(error)"
|
||||
)
|
||||
}
|
||||
|
||||
do {
|
||||
print("Verifying machine API server is running...")
|
||||
_ = try await MachineClient().list()
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
message: "failed to get a response from machine API server: \(error)"
|
||||
)
|
||||
}
|
||||
|
||||
if await !initImageExists(containerSystemConfig: containerSystemConfig) {
|
||||
try? await installInitialFilesystem(initImage: containerSystemConfig.vminit.image)
|
||||
}
|
||||
|
||||
guard await !kernelExists() else {
|
||||
return
|
||||
}
|
||||
try await installDefaultKernel(kernelURL: containerSystemConfig.kernel.url, kernelBinaryPath: containerSystemConfig.kernel.binaryPath)
|
||||
}
|
||||
|
||||
private func installInitialFilesystem(initImage: String) async throws {
|
||||
var pullCommand = try ImagePull.parse()
|
||||
pullCommand.reference = initImage
|
||||
log.info("Installing base container filesystem...")
|
||||
do {
|
||||
try await pullCommand.run()
|
||||
} catch {
|
||||
log.error("failed to install base container filesystem", metadata: ["error": "\(error)"])
|
||||
}
|
||||
}
|
||||
|
||||
private func installDefaultKernel(kernelURL: URL, kernelBinaryPath: String) async throws {
|
||||
var shouldInstallKernel = false
|
||||
if kernelInstall == nil {
|
||||
print("No default kernel configured.")
|
||||
print("Install the recommended default kernel from [\(kernelURL)]? [Y/n]: ", terminator: "")
|
||||
guard let read = readLine(strippingNewline: true) else {
|
||||
throw ContainerizationError(.internalError, message: "failed to read user input")
|
||||
}
|
||||
guard read.lowercased() == "y" || read.count == 0 else {
|
||||
log.info("Please use the `container system kernel set --recommended` command to configure the default kernel")
|
||||
return
|
||||
}
|
||||
shouldInstallKernel = true
|
||||
} else {
|
||||
shouldInstallKernel = kernelInstall ?? false
|
||||
}
|
||||
guard shouldInstallKernel else {
|
||||
return
|
||||
}
|
||||
log.info("Installing kernel...")
|
||||
try await KernelSet.downloadAndInstallWithProgressBar(tarRemoteURL: kernelURL, kernelFilePath: kernelBinaryPath, force: true)
|
||||
}
|
||||
|
||||
private func initImageExists(containerSystemConfig: ContainerSystemConfig) async -> Bool {
|
||||
do {
|
||||
let img = try await ClientImage.get(
|
||||
reference: containerSystemConfig.vminit.image,
|
||||
containerSystemConfig: containerSystemConfig
|
||||
)
|
||||
let _ = try await img.getSnapshot(platform: .current)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func kernelExists() async -> Bool {
|
||||
do {
|
||||
try await ClientKernel.getDefaultKernel(for: .current)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerPlugin
|
||||
import ContainerizationError
|
||||
import Foundation
|
||||
import Logging
|
||||
|
||||
extension Application {
|
||||
public struct SystemStatus: AsyncLoggableCommand {
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "status",
|
||||
abstract: "Show the status of `container` services"
|
||||
)
|
||||
|
||||
@Option(name: .shortAndLong, help: "Launchd prefix for services")
|
||||
var prefix: String = "com.apple.container."
|
||||
|
||||
@Option(name: .long, help: "Format of the output")
|
||||
var format: ListFormat = .table
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
public init() {}
|
||||
|
||||
struct PrintableStatus: Codable {
|
||||
let status: String
|
||||
let appRoot: String
|
||||
let installRoot: String
|
||||
let logRoot: String?
|
||||
let apiServerVersion: String
|
||||
let apiServerCommit: String
|
||||
let apiServerBuild: String
|
||||
let apiServerAppName: String
|
||||
|
||||
init(
|
||||
status: String,
|
||||
appRoot: String = "",
|
||||
installRoot: String = "",
|
||||
logRoot: String? = nil,
|
||||
apiServerVersion: String = "",
|
||||
apiServerCommit: String = "",
|
||||
apiServerBuild: String = "",
|
||||
apiServerAppName: String = ""
|
||||
) {
|
||||
self.status = status
|
||||
self.appRoot = appRoot
|
||||
self.installRoot = installRoot
|
||||
self.logRoot = logRoot
|
||||
self.apiServerVersion = apiServerVersion
|
||||
self.apiServerCommit = apiServerCommit
|
||||
self.apiServerBuild = apiServerBuild
|
||||
self.apiServerAppName = apiServerAppName
|
||||
}
|
||||
}
|
||||
|
||||
public func run() async throws {
|
||||
let isRegistered = try ServiceManager.isRegistered(fullServiceLabel: "\(prefix)apiserver")
|
||||
if !isRegistered {
|
||||
try Output.render(payload: PrintableStatus(status: "unregistered"), format: format) {
|
||||
"apiserver is not running and not registered with launchd"
|
||||
}
|
||||
Application.exit(withError: ExitCode(1))
|
||||
}
|
||||
|
||||
// Now ping our friendly daemon. Fail after 10 seconds with no response.
|
||||
do {
|
||||
let systemHealth = try await ClientHealthCheck.ping(timeout: .seconds(10))
|
||||
let status = PrintableStatus(
|
||||
status: "running",
|
||||
appRoot: systemHealth.appRoot.path(percentEncoded: false),
|
||||
installRoot: systemHealth.installRoot.path(percentEncoded: false),
|
||||
logRoot: systemHealth.logRoot?.string,
|
||||
apiServerVersion: systemHealth.apiServerVersion,
|
||||
apiServerCommit: systemHealth.apiServerCommit,
|
||||
apiServerBuild: systemHealth.apiServerBuild,
|
||||
apiServerAppName: systemHealth.apiServerAppName
|
||||
)
|
||||
try Output.render(payload: status, format: format) {
|
||||
Self.statusTable(status)
|
||||
}
|
||||
} catch {
|
||||
try Output.render(payload: PrintableStatus(status: "not running"), format: format) {
|
||||
"apiserver is not running"
|
||||
}
|
||||
Application.exit(withError: ExitCode(1))
|
||||
}
|
||||
}
|
||||
|
||||
private static func statusTable(_ status: PrintableStatus) -> String {
|
||||
let rows: [[String]] = [
|
||||
["FIELD", "VALUE"],
|
||||
["status", status.status],
|
||||
["appRoot", status.appRoot],
|
||||
["installRoot", status.installRoot],
|
||||
["logRoot", status.logRoot ?? ""],
|
||||
["apiserver.version", status.apiServerVersion],
|
||||
["apiserver.commit", status.apiServerCommit],
|
||||
["apiserver.build", status.apiServerBuild],
|
||||
["apiserver.appName", status.apiServerAppName],
|
||||
]
|
||||
return TableOutput(rows: rows).format()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerPlugin
|
||||
import ContainerResource
|
||||
import Containerization
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
import Logging
|
||||
|
||||
extension Application {
|
||||
public struct SystemStop: AsyncLoggableCommand {
|
||||
private static let stopTimeoutSeconds: Int32 = 5
|
||||
private static let shutdownTimeoutSeconds: Int32 = 20
|
||||
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "stop",
|
||||
abstract: "Stop all `container` services"
|
||||
)
|
||||
|
||||
@Option(name: .shortAndLong, help: "Launchd prefix for services")
|
||||
var prefix: String = "com.apple.container."
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
let log = Logger(
|
||||
label: "com.apple.container.cli",
|
||||
factory: { label in
|
||||
StreamLogHandler.standardOutput(label: label)
|
||||
}
|
||||
)
|
||||
|
||||
let launchdDomainString = try ServiceManager.getDomainString()
|
||||
let fullLabel = "\(launchdDomainString)/\(prefix)apiserver"
|
||||
|
||||
var running = true
|
||||
do {
|
||||
log.info("checking if APIServer is alive")
|
||||
_ = try await ClientHealthCheck.ping(timeout: .seconds(5))
|
||||
} catch {
|
||||
log.info("APIServer health check failed, skipping bootout")
|
||||
running = false
|
||||
}
|
||||
|
||||
if running {
|
||||
let client = ContainerClient()
|
||||
log.info("stopping containers", metadata: ["stopTimeoutSeconds": "\(Self.stopTimeoutSeconds)"])
|
||||
do {
|
||||
let containers = try await client.list().map { $0.id }
|
||||
let opts = ContainerStopOptions(timeoutInSeconds: Self.stopTimeoutSeconds, signal: nil)
|
||||
try await ContainerStop.stopContainers(
|
||||
client: client,
|
||||
containers: containers,
|
||||
stopOptions: opts,
|
||||
)
|
||||
} catch {
|
||||
log.warning("failed to stop all containers", metadata: ["error": "\(error)"])
|
||||
}
|
||||
|
||||
log.info("waiting for containers to exit")
|
||||
do {
|
||||
for _ in 0..<Self.shutdownTimeoutSeconds {
|
||||
let runningContainers = try await client.list(filters: ContainerListFilters(status: .running))
|
||||
guard !runningContainers.isEmpty else {
|
||||
break
|
||||
}
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
}
|
||||
|
||||
log.info("stopping service", metadata: ["label": "\(fullLabel)"])
|
||||
try ServiceManager.deregister(fullServiceLabel: fullLabel)
|
||||
} catch {
|
||||
log.warning("failed to wait for all containers", metadata: ["error": "\(error)"])
|
||||
}
|
||||
}
|
||||
|
||||
// Note: The assumption here is that we would have registered the launchd services
|
||||
// in the same domain as `launchdDomainString`. This is a fairly sane assumption since
|
||||
// if somehow the launchd domain changed, XPC interactions would not be possible.
|
||||
try ServiceManager.enumerate()
|
||||
.filter { $0.hasPrefix(prefix) }
|
||||
.filter { $0 != fullLabel }
|
||||
.map { "\(launchdDomainString)/\($0)" }
|
||||
.forEach {
|
||||
log.info("stopping service", metadata: ["label": "\($0)"])
|
||||
try? ServiceManager.deregister(fullServiceLabel: $0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerVersion
|
||||
import Foundation
|
||||
|
||||
extension Application {
|
||||
public struct SystemVersion: AsyncLoggableCommand {
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "version",
|
||||
abstract: "Show version information"
|
||||
)
|
||||
|
||||
@Option(name: .long, help: "Format of the output")
|
||||
var format: ListFormat = .table
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
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 }
|
||||
|
||||
try Output.render(payload: versions, format: format) {
|
||||
Self.versionTable(versions)
|
||||
}
|
||||
}
|
||||
|
||||
private static func versionTable(_ versions: [VersionInfo]) -> String {
|
||||
let header = ["COMPONENT", "VERSION", "BUILD", "COMMIT"]
|
||||
let rows = [header] + versions.map { [$0.appName, $0.version, $0.buildType, $0.commit] }
|
||||
return TableOutput(rows: rows).format()
|
||||
}
|
||||
}
|
||||
|
||||
public struct VersionInfo: Codable {
|
||||
let version: String
|
||||
let buildType: String
|
||||
let commit: String
|
||||
let appName: String
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user