Teach VM listing Docker-style aliases and filters
Make `banger ps` a true alias of `banger vm list` and add `banger vm ls` and `banger vm ps` so the common listing entrypoints all share one path. Default the shared list command to running VMs only, add `--all` to include stopped entries, `--latest` to keep only the newest matching VM, and `--quiet` to print full VM IDs without the table renderer. Cover the alias wiring plus the running/latest/quiet helpers in CLI tests. Validation: go test ./internal/cli; GOCACHE=/tmp/banger-gocache go test ./...; make build; ./build/bin/banger ps --help; ./build/bin/banger vm ls --help.
This commit is contained in:
parent
671723a0ef
commit
dbc70643c3
2 changed files with 164 additions and 19 deletions
|
|
@ -150,7 +150,7 @@ func NewBangerCommand() *cobra.Command {
|
||||||
RunE: helpNoArgs,
|
RunE: helpNoArgs,
|
||||||
}
|
}
|
||||||
root.CompletionOptions.DisableDefaultCmd = true
|
root.CompletionOptions.DisableDefaultCmd = true
|
||||||
root.AddCommand(newDaemonCommand(), newDoctorCommand(), newImageCommand(), newInternalCommand(), newVersionCommand(), newVMCommand())
|
root.AddCommand(newDaemonCommand(), newDoctorCommand(), newImageCommand(), newInternalCommand(), newVersionCommand(), newPSCommand(), newVMCommand())
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -626,27 +626,79 @@ func newVMCreateCommand() *cobra.Command {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type vmListOptions struct {
|
||||||
|
showAll bool
|
||||||
|
latest bool
|
||||||
|
quiet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPSCommand() *cobra.Command {
|
||||||
|
return newVMListLikeCommand("ps", nil, "usage: banger ps")
|
||||||
|
}
|
||||||
|
|
||||||
func newVMListCommand() *cobra.Command {
|
func newVMListCommand() *cobra.Command {
|
||||||
return &cobra.Command{
|
return newVMListLikeCommand("list", []string{"ls", "ps"}, "usage: banger vm list")
|
||||||
Use: "list",
|
}
|
||||||
Short: "List VMs",
|
|
||||||
Args: noArgsUsage("usage: banger vm list"),
|
func newVMListLikeCommand(use string, aliases []string, usage string) *cobra.Command {
|
||||||
|
var opts vmListOptions
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: use,
|
||||||
|
Aliases: aliases,
|
||||||
|
Short: "List VMs",
|
||||||
|
Args: noArgsUsage(usage),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
layout, _, err := ensureDaemon(cmd.Context())
|
return runVMList(cmd, opts)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
result, err := rpc.Call[api.VMListResult](cmd.Context(), layout.SocketPath, "vm.list", api.Empty{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
images, err := rpc.Call[api.ImageListResult](cmd.Context(), layout.SocketPath, "image.list", api.Empty{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return printVMListTable(cmd.OutOrStdout(), result.VMs, imageNameIndex(images.Images))
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
cmd.Flags().BoolVarP(&opts.showAll, "all", "a", false, "show all VMs")
|
||||||
|
cmd.Flags().BoolVarP(&opts.latest, "latest", "l", false, "show only the latest VM")
|
||||||
|
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "only show VM IDs")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVMList(cmd *cobra.Command, opts vmListOptions) error {
|
||||||
|
layout, _, err := ensureDaemon(cmd.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result, err := rpc.Call[api.VMListResult](cmd.Context(), layout.SocketPath, "vm.list", api.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
vms := selectVMListVMs(result.VMs, opts.showAll, opts.latest)
|
||||||
|
if opts.quiet {
|
||||||
|
return printVMIDList(cmd.OutOrStdout(), vms)
|
||||||
|
}
|
||||||
|
images, err := rpc.Call[api.ImageListResult](cmd.Context(), layout.SocketPath, "image.list", api.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return printVMListTable(cmd.OutOrStdout(), vms, imageNameIndex(images.Images))
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectVMListVMs(vms []model.VMRecord, showAll, latest bool) []model.VMRecord {
|
||||||
|
filtered := make([]model.VMRecord, 0, len(vms))
|
||||||
|
for _, vm := range vms {
|
||||||
|
if !showAll && vm.State != model.VMStateRunning {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, vm)
|
||||||
|
}
|
||||||
|
if !latest || len(filtered) <= 1 {
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
latestVM := filtered[0]
|
||||||
|
for _, vm := range filtered[1:] {
|
||||||
|
if vm.CreatedAt.After(latestVM.CreatedAt) {
|
||||||
|
latestVM = vm
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if vm.CreatedAt.Equal(latestVM.CreatedAt) && vm.UpdatedAt.After(latestVM.UpdatedAt) {
|
||||||
|
latestVM = vm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []model.VMRecord{latestVM}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newVMShowCommand() *cobra.Command {
|
func newVMShowCommand() *cobra.Command {
|
||||||
|
|
@ -2054,6 +2106,15 @@ func printVMSummary(out anyWriter, vm model.VMRecord) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printVMIDList(out anyWriter, vms []model.VMRecord) error {
|
||||||
|
for _, vm := range vms {
|
||||||
|
if _, err := fmt.Fprintln(out, vm.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func printVMListTable(out anyWriter, vms []model.VMRecord, imageNames map[string]string) error {
|
func printVMListTable(out anyWriter, vms []model.VMRecord, imageNames map[string]string) error {
|
||||||
w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
|
w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
|
||||||
if _, err := fmt.Fprintln(w, "ID\tNAME\tSTATE\tIMAGE\tIP\tVCPU\tMEM\tDISK\tCREATED"); err != nil {
|
if _, err := fmt.Fprintln(w, "ID\tNAME\tSTATE\tIMAGE\tIP\tVCPU\tMEM\tDISK\tCREATED"); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ func TestNewBangerCommandHasExpectedSubcommands(t *testing.T) {
|
||||||
for _, sub := range cmd.Commands() {
|
for _, sub := range cmd.Commands() {
|
||||||
names = append(names, sub.Name())
|
names = append(names, sub.Name())
|
||||||
}
|
}
|
||||||
want := []string{"daemon", "doctor", "image", "internal", "version", "vm"}
|
want := []string{"daemon", "doctor", "image", "internal", "ps", "version", "vm"}
|
||||||
if !reflect.DeepEqual(names, want) {
|
if !reflect.DeepEqual(names, want) {
|
||||||
t.Fatalf("subcommands = %v, want %v", names, want)
|
t.Fatalf("subcommands = %v, want %v", names, want)
|
||||||
}
|
}
|
||||||
|
|
@ -155,6 +155,47 @@ func TestInternalPackagesCommandSupportsAlpine(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPSAndVMListAliasesAndFlagsExist(t *testing.T) {
|
||||||
|
root := NewBangerCommand()
|
||||||
|
ps, _, err := root.Find([]string{"ps"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("find ps: %v", err)
|
||||||
|
}
|
||||||
|
for _, flagName := range []string{"all", "latest", "quiet"} {
|
||||||
|
if ps.Flags().Lookup(flagName) == nil {
|
||||||
|
t.Fatalf("missing ps flag %q", flagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vm, _, err := root.Find([]string{"vm"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("find vm: %v", err)
|
||||||
|
}
|
||||||
|
list, _, err := vm.Find([]string{"list"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("find list: %v", err)
|
||||||
|
}
|
||||||
|
if _, _, err := vm.Find([]string{"ls"}); err != nil {
|
||||||
|
t.Fatalf("find ls alias: %v", err)
|
||||||
|
}
|
||||||
|
if _, _, err := vm.Find([]string{"ps"}); err != nil {
|
||||||
|
t.Fatalf("find ps alias: %v", err)
|
||||||
|
}
|
||||||
|
for _, flagName := range []string{"all", "latest", "quiet"} {
|
||||||
|
if list.Flags().Lookup(flagName) == nil {
|
||||||
|
t.Fatalf("missing vm list flag %q", flagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPSCommandRejectsArgs(t *testing.T) {
|
||||||
|
cmd := NewBangerCommand()
|
||||||
|
cmd.SetArgs([]string{"ps", "extra"})
|
||||||
|
err := cmd.Execute()
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "usage: banger ps") {
|
||||||
|
t.Fatalf("Execute() error = %v, want ps usage error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVMCreateFlagsExist(t *testing.T) {
|
func TestVMCreateFlagsExist(t *testing.T) {
|
||||||
root := NewBangerCommand()
|
root := NewBangerCommand()
|
||||||
vm, _, err := root.Find([]string{"vm"})
|
vm, _, err := root.Find([]string{"vm"})
|
||||||
|
|
@ -595,6 +636,49 @@ func TestPrintImageListTableShowsRootfsSizes(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSelectVMListVMsDefaultsToRunning(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
vms := []model.VMRecord{
|
||||||
|
{ID: "running-1", State: model.VMStateRunning, CreatedAt: now.Add(-3 * time.Hour)},
|
||||||
|
{ID: "stopped-1", State: model.VMStateStopped, CreatedAt: now.Add(-2 * time.Hour)},
|
||||||
|
{ID: "running-2", State: model.VMStateRunning, CreatedAt: now.Add(-1 * time.Hour)},
|
||||||
|
}
|
||||||
|
got := selectVMListVMs(vms, false, false)
|
||||||
|
if len(got) != 2 || got[0].ID != "running-1" || got[1].ID != "running-2" {
|
||||||
|
t.Fatalf("selectVMListVMs() = %#v, want only running VMs in original order", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectVMListVMsLatestUsesFilteredSet(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
vms := []model.VMRecord{
|
||||||
|
{ID: "running-old", State: model.VMStateRunning, CreatedAt: now.Add(-3 * time.Hour)},
|
||||||
|
{ID: "stopped-new", State: model.VMStateStopped, CreatedAt: now.Add(-30 * time.Minute)},
|
||||||
|
{ID: "running-new", State: model.VMStateRunning, CreatedAt: now.Add(-1 * time.Hour)},
|
||||||
|
}
|
||||||
|
got := selectVMListVMs(vms, false, true)
|
||||||
|
if len(got) != 1 || got[0].ID != "running-new" {
|
||||||
|
t.Fatalf("selectVMListVMs(default latest) = %#v, want latest running VM", got)
|
||||||
|
}
|
||||||
|
got = selectVMListVMs(vms, true, true)
|
||||||
|
if len(got) != 1 || got[0].ID != "stopped-new" {
|
||||||
|
t.Fatalf("selectVMListVMs(all latest) = %#v, want latest VM across all states", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintVMIDListShowsFullIDs(t *testing.T) {
|
||||||
|
var out bytes.Buffer
|
||||||
|
err := printVMIDList(&out, []model.VMRecord{{ID: "0123456789abcdef0123456789abcdef"}, {ID: "fedcba9876543210fedcba9876543210"}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("printVMIDList() error = %v", err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
|
||||||
|
want := []string{"0123456789abcdef0123456789abcdef", "fedcba9876543210fedcba9876543210"}
|
||||||
|
if !reflect.DeepEqual(lines, want) {
|
||||||
|
t.Fatalf("lines = %v, want %v", lines, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPrintVMListTableShowsImageNames(t *testing.T) {
|
func TestPrintVMListTableShowsImageNames(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := printVMListTable(&out, []model.VMRecord{
|
err := printVMListTable(&out, []model.VMRecord{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue