Refine vm ports output

Make banger vm ports single-target and collapse the old VM/WEB table shape into a simpler PROTO ENDPOINT PROCESS COMMAND view. Web listeners now surface directly as http or https, with clickable endpoints in the main endpoint column instead of a separate URL field.

Classify TCP listeners with HTTPS-first probing so TLS services are not mislabeled as plain HTTP just because they answer bad cleartext requests with an HTTP error, then dedupe rows by rendered PROTO+ENDPOINT so dual-stack binds like 0.0.0.0 and :: only show once.

Update the CLI/daemon regressions and README to match the new contract. Verified with GOCACHE=/tmp/banger-gocache go test ./..., make build, git diff --check, and ./banger vm ports --help.
This commit is contained in:
Thales Maciel 2026-03-19 18:21:04 -03:00
parent 5ad3b505dd
commit 3096de0a7f
No known key found for this signature in database
GPG key ID: 33112E6833C34679
6 changed files with 179 additions and 151 deletions

View file

@ -549,44 +549,19 @@ func newVMStatsCommand() *cobra.Command {
func newVMPortsCommand() *cobra.Command {
return &cobra.Command{
Use: "ports <id-or-name>...",
Use: "ports <id-or-name>",
Short: "Show host-reachable listening guest ports",
Args: minArgsUsage(1, "usage: banger vm ports <id-or-name>..."),
Args: exactArgsUsage(1, "usage: banger vm ports <id-or-name>"),
RunE: func(cmd *cobra.Command, args []string) error {
layout, _, err := ensureDaemon(cmd.Context())
if err != nil {
return err
}
listResult, err := rpc.Call[api.VMListResult](cmd.Context(), layout.SocketPath, "vm.list", api.Empty{})
result, err := vmPortsFunc(cmd.Context(), layout.SocketPath, args[0])
if err != nil {
return err
}
targets, resolutionErrs := resolveVMTargets(listResult.VMs, args)
results := executeVMPortsBatch(cmd.Context(), layout.SocketPath, targets)
failed := false
for _, resolutionErr := range resolutionErrs {
if _, err := fmt.Fprintf(cmd.ErrOrStderr(), "%s: %v\n", resolutionErr.Ref, resolutionErr.Err); err != nil {
return err
}
failed = true
}
for _, result := range results {
if result.Err == nil {
continue
}
if _, err := fmt.Fprintf(cmd.ErrOrStderr(), "%s: %v\n", result.Target.Ref, result.Err); err != nil {
return err
}
failed = true
}
if err := printVMPortsTable(cmd.OutOrStdout(), results); err != nil {
return err
}
if failed {
return errors.New("one or more VM operations failed")
}
return nil
return printVMPortsTable(cmd.OutOrStdout(), result)
},
}
}
@ -793,12 +768,6 @@ type vmBatchActionResult struct {
Err error
}
type vmPortsBatchResult struct {
Target resolvedVMTarget
Result api.VMPortsResult
Err error
}
func runVMBatchAction(cmd *cobra.Command, socketPath string, refs []string, action func(context.Context, string) (model.VMRecord, error)) error {
listResult, err := rpc.Call[api.VMListResult](cmd.Context(), socketPath, "vm.list", api.Empty{})
if err != nil {
@ -907,27 +876,6 @@ func executeVMActionBatch(ctx context.Context, targets []resolvedVMTarget, actio
return results
}
func executeVMPortsBatch(ctx context.Context, socketPath string, targets []resolvedVMTarget) []vmPortsBatchResult {
results := make([]vmPortsBatchResult, len(targets))
var wg sync.WaitGroup
wg.Add(len(targets))
for index, target := range targets {
index := index
target := target
go func() {
defer wg.Done()
result, err := vmPortsFunc(ctx, socketPath, target.VM.ID)
results[index] = vmPortsBatchResult{
Target: target,
Result: result,
Err: err,
}
}()
}
wg.Wait()
return results
}
func ensureDaemon(ctx context.Context) (paths.Layout, model.DaemonConfig, error) {
layout, err := paths.Resolve()
if err != nil {
@ -1223,41 +1171,25 @@ func printImageSummary(out anyWriter, image model.Image) error {
return err
}
func printVMPortsTable(out anyWriter, results []vmPortsBatchResult) error {
func printVMPortsTable(out anyWriter, result api.VMPortsResult) error {
type portRow struct {
VM string
Proto string
Endpoint string
Process string
Command string
WebURL string
Port int
}
rows := make([]portRow, 0)
for _, result := range results {
if result.Err != nil {
continue
}
vmName := strings.TrimSpace(result.Result.Name)
if vmName == "" {
vmName = result.Target.VM.Name
}
for _, port := range result.Result.Ports {
rows = append(rows, portRow{
VM: vmName,
Proto: port.Proto,
Endpoint: port.Endpoint,
Process: port.Process,
Command: port.Command,
WebURL: emptyDash(port.WebURL),
Port: port.Port,
})
}
rows := make([]portRow, 0, len(result.Ports))
for _, port := range result.Ports {
rows = append(rows, portRow{
Proto: port.Proto,
Endpoint: port.Endpoint,
Process: port.Process,
Command: port.Command,
Port: port.Port,
})
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].VM != rows[j].VM {
return rows[i].VM < rows[j].VM
}
if rows[i].Proto != rows[j].Proto {
return rows[i].Proto < rows[j].Proto
}
@ -1274,19 +1206,17 @@ func printVMPortsTable(out anyWriter, results []vmPortsBatchResult) error {
}
w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
if _, err := fmt.Fprintln(w, "VM\tPROTO\tENDPOINT\tPROCESS\tCOMMAND\tWEB"); err != nil {
if _, err := fmt.Fprintln(w, "PROTO\tENDPOINT\tPROCESS\tCOMMAND"); err != nil {
return err
}
for _, row := range rows {
if _, err := fmt.Fprintf(
w,
"%s\t%s\t%s\t%s\t%s\t%s\n",
row.VM,
"%s\t%s\t%s\t%s\n",
row.Proto,
emptyDash(row.Endpoint),
emptyDash(row.Process),
emptyDash(row.Command),
row.WebURL,
); err != nil {
return err
}