mirror of https://github.com/cirruslabs/tart.git
JSON output for get and list commands (#394)
* JSON output for `get` and `list` commands In the light of the upcoming `1.0.0` release and stabilizing of the API, let's introduce some breaking changes for the good. Removed all the `--cpu`, `--memory`, `--disk` and `--display` flags and replaced with a single `--json` flag for machine-readable output. Added `--json` option to the `list` command to output a single JSON list. Notably removed `--quite` flag since it seemed unnecessary. Fixes #297 * Added Size to `list` output Fixes #379 * Added running state to `get` Fixes #393 * Better signature * Updated tests * More test fixes
This commit is contained in:
parent
ecc5de18be
commit
f34aa5f072
|
|
@ -116,6 +116,15 @@
|
|||
"revision" : "da637c398c5d08896521b737f2868ddc2e7996ae",
|
||||
"version" : "0.50.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "texttable",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/cfilipov/TextTable",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "e03289289155b4e7aa565e32862f9cb42140596a"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ let package = Package(
|
|||
.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.0.0")),
|
||||
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.6"),
|
||||
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "7.31.3"),
|
||||
.package(url: "https://github.com/cfilipov/TextTable", branch: "master"),
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(name: "tart", dependencies: [
|
||||
|
|
@ -32,7 +33,7 @@ let package = Package(
|
|||
.product(name: "Antlr4Static", package: "Antlr4"),
|
||||
.product(name: "Atomics", package: "swift-atomics"),
|
||||
.product(name: "Sentry", package: "sentry-cocoa"),
|
||||
|
||||
.product(name: "TextTable", package: "TextTable"),
|
||||
], exclude: [
|
||||
"OCI/Reference/Makefile",
|
||||
"OCI/Reference/Reference.g4",
|
||||
|
|
|
|||
|
|
@ -1,52 +1,31 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
fileprivate struct VMInfo: Encodable {
|
||||
let CPU: Int
|
||||
let Memory: UInt64
|
||||
let Disk: Int
|
||||
let Display: String
|
||||
let Running: Bool
|
||||
}
|
||||
|
||||
struct Get: AsyncParsableCommand {
|
||||
static var configuration = CommandConfiguration(commandName: "get", abstract: "Get a VM's configuration")
|
||||
|
||||
@Argument(help: "VM name.")
|
||||
var name: String
|
||||
|
||||
@Flag(help: "Number of VM CPUs.")
|
||||
var cpu: Bool = false
|
||||
|
||||
@Flag(help: "VM memory size in megabytes.")
|
||||
var memory: Bool = false
|
||||
|
||||
@Flag(help: "Disk size in gigabytes.")
|
||||
var diskSize: Bool = false
|
||||
|
||||
@Flag(help: "VM display resolution in a format of <width>x<height>. For example, 1200x800.")
|
||||
var display: Bool = false
|
||||
|
||||
func validate() throws {
|
||||
if [cpu, memory, diskSize, display].filter({$0}).count > 1 {
|
||||
throw ValidationError("Options --cpu, --memory, --disk-size and --display are mutually exclusive")
|
||||
}
|
||||
}
|
||||
@Option(help: "Output format: text or json")
|
||||
var format: Format = .text
|
||||
|
||||
func run() async throws {
|
||||
let vmDir = try VMStorageLocal().open(name)
|
||||
let vmConfig = try VMConfig(fromURL: vmDir.configURL)
|
||||
let diskSizeInGb = try vmDir.sizeBytes() / 1000 / 1000 / 1000
|
||||
let memorySizeInMb = vmConfig.memorySize / 1024 / 1024
|
||||
let diskSizeInGb = try vmDir.sizeGB()
|
||||
let memorySizeInMb = vmConfig.memorySize / 1024 / 1024
|
||||
let running = try PIDLock(lockURL: vmDir.configURL).pid() > 0
|
||||
|
||||
if cpu {
|
||||
print(vmConfig.cpuCount)
|
||||
} else if memory {
|
||||
print(memorySizeInMb)
|
||||
} else if diskSize {
|
||||
print(diskSizeInGb)
|
||||
} else if display {
|
||||
print("\(vmConfig.display.width)x\(vmConfig.display.height)")
|
||||
} else {
|
||||
print(
|
||||
"CPU\tMemory\tDisk\tDisplay\n" +
|
||||
"\(vmConfig.cpuCount)\t" +
|
||||
"\(memorySizeInMb) MB\t" +
|
||||
"\(diskSizeInGb) GB\t" +
|
||||
"\(vmConfig.display)"
|
||||
)
|
||||
}
|
||||
let info = VMInfo(CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: diskSizeInGb, Display: vmConfig.display.description, Running: running)
|
||||
print(format.renderSingle(info))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,24 @@ import ArgumentParser
|
|||
import Dispatch
|
||||
import SwiftUI
|
||||
|
||||
fileprivate struct VMInfo: Encodable {
|
||||
let Source: String
|
||||
let Name: String
|
||||
let Size: Int
|
||||
}
|
||||
|
||||
struct List: AsyncParsableCommand {
|
||||
static var configuration = CommandConfiguration(abstract: "List created VMs")
|
||||
|
||||
@Flag(name: [.short, .long], help: ArgumentHelp("Only display VM names."))
|
||||
var quiet: Bool = false
|
||||
|
||||
@Option(help: ArgumentHelp("Only display VMs from the specified source (e.g. --source local, --source oci)."))
|
||||
var source: String?
|
||||
|
||||
@Option(help: "Output format: text or json")
|
||||
var format: Format = .text
|
||||
|
||||
@Flag(name: [.short, .long], help: ArgumentHelp("Only display VM names."))
|
||||
var quiet: Bool = false
|
||||
|
||||
func validate() throws {
|
||||
guard let source = source else {
|
||||
return
|
||||
|
|
@ -22,27 +31,28 @@ struct List: AsyncParsableCommand {
|
|||
}
|
||||
|
||||
func run() async throws {
|
||||
if !quiet {
|
||||
print("Source\tName")
|
||||
}
|
||||
|
||||
var infos: [VMInfo] = []
|
||||
if source == nil || source == "local" {
|
||||
displayTable("local", try VMStorageLocal().list())
|
||||
infos += sortedInfos(try VMStorageLocal().list().map { (name, vmDir) in
|
||||
try VMInfo(Source: "local", Name: name, Size: vmDir.sizeGB())
|
||||
})
|
||||
}
|
||||
|
||||
if source == nil || source == "oci" {
|
||||
displayTable("oci", try VMStorageOCI().list().map { (name, vmDir, _) in (name, vmDir) })
|
||||
infos += sortedInfos(try VMStorageOCI().list().map { (name, vmDir, _) in
|
||||
try VMInfo(Source: "oci", Name: name, Size: vmDir.sizeGB())
|
||||
})
|
||||
}
|
||||
if (quiet) {
|
||||
for info in infos {
|
||||
print(info.Name)
|
||||
}
|
||||
} else {
|
||||
print(format.renderList(infos))
|
||||
}
|
||||
}
|
||||
|
||||
private func displayTable(_ source: String, _ vms: [(String, VMDirectory)]) {
|
||||
for (name, _) in vms.sorted(by: { left, right in left.0 < right.0 }) {
|
||||
if quiet {
|
||||
print(name)
|
||||
} else {
|
||||
let source = source.padding(toLength: "Source".count, withPad: " ", startingAt: 0)
|
||||
print("\(source)\t\(name)")
|
||||
}
|
||||
}
|
||||
private func sortedInfos(_ infos: [VMInfo]) -> [VMInfo] {
|
||||
infos.sorted(by: { left, right in left.Name < right.Name })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import TextTable
|
||||
|
||||
enum Format: String, ExpressibleByArgument, CaseIterable {
|
||||
case text, json
|
||||
|
||||
private(set) static var allValueStrings: [String] = Format.allCases.map { "\($0)"}
|
||||
|
||||
func renderSingle<T>(_ data: T) -> String where T: Encodable {
|
||||
switch self {
|
||||
case .text:
|
||||
return renderList([data])
|
||||
case .json:
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
return try! encoder.encode(data).asText()
|
||||
}
|
||||
}
|
||||
|
||||
func renderList<T>(_ data: Array<T>) -> String where T: Encodable {
|
||||
switch self {
|
||||
case .text:
|
||||
if (data.count == 0) {
|
||||
return ""
|
||||
}
|
||||
let table = TextTable<T> { (item: T) in
|
||||
let mirroredObject = Mirror(reflecting: item)
|
||||
return mirroredObject.children.enumerated().map { (_, element) in
|
||||
let fieldName = element.label!
|
||||
return Column(title: fieldName, value: element.value)
|
||||
}
|
||||
}
|
||||
return table.string(for: data, style: Style.plain)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
case .json:
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
return try! encoder.encode(data).asText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +107,10 @@ struct VMDirectory: Prunable {
|
|||
try configURL.sizeBytes() + diskURL.sizeBytes() + nvramURL.sizeBytes()
|
||||
}
|
||||
|
||||
func sizeGB() throws -> Int {
|
||||
try sizeBytes() / 1000 / 1000 / 1000
|
||||
}
|
||||
|
||||
func markExplicitlyPulled() {
|
||||
FileManager.default.createFile(atPath: explicitlyPulledMark.path, contents: nil)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue