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:
parent
5ad3b505dd
commit
3096de0a7f
6 changed files with 179 additions and 151 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,6 +162,15 @@ func TestVMPortsCommandExists(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestVMPortsCommandRejectsMultipleRefs(t *testing.T) {
|
||||
cmd := NewBangerCommand()
|
||||
cmd.SetArgs([]string{"vm", "ports", "alpha", "beta"})
|
||||
err := cmd.Execute()
|
||||
if err == nil || !strings.Contains(err.Error(), "usage: banger vm ports <id-or-name>") {
|
||||
t.Fatalf("Execute() error = %v, want single-vm usage error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMSetParamsFromFlags(t *testing.T) {
|
||||
params, err := vmSetParamsFromFlags("devbox", 4, 2048, "16G", true, false)
|
||||
if err != nil {
|
||||
|
|
@ -279,56 +288,43 @@ func TestAbsolutizeImageRegisterPaths(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPrintVMPortsTableSortsAndRendersURLs(t *testing.T) {
|
||||
results := []vmPortsBatchResult{
|
||||
{
|
||||
Target: resolvedVMTarget{Ref: "beta"},
|
||||
Result: api.VMPortsResult{
|
||||
Name: "beta",
|
||||
Ports: []api.VMPort{{
|
||||
Proto: "tcp",
|
||||
Port: 8080,
|
||||
Endpoint: "beta.vm:8080",
|
||||
Process: "python3",
|
||||
Command: "python3 -m http.server 8080",
|
||||
WebURL: "http://beta.vm:8080/",
|
||||
}},
|
||||
func TestPrintVMPortsTableSortsAndRendersURLEndpoints(t *testing.T) {
|
||||
result := api.VMPortsResult{
|
||||
Name: "alpha",
|
||||
Ports: []api.VMPort{
|
||||
{
|
||||
Proto: "https",
|
||||
Port: 443,
|
||||
Endpoint: "https://alpha.vm:443/",
|
||||
Process: "caddy",
|
||||
Command: "caddy run",
|
||||
},
|
||||
},
|
||||
{
|
||||
Target: resolvedVMTarget{Ref: "alpha"},
|
||||
Result: api.VMPortsResult{
|
||||
Name: "alpha",
|
||||
Ports: []api.VMPort{{
|
||||
Proto: "udp",
|
||||
Port: 53,
|
||||
Endpoint: "alpha.vm:53",
|
||||
Process: "dnsd",
|
||||
Command: "dnsd --foreground",
|
||||
}},
|
||||
{
|
||||
Proto: "udp",
|
||||
Port: 53,
|
||||
Endpoint: "alpha.vm:53",
|
||||
Process: "dnsd",
|
||||
Command: "dnsd --foreground",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := printVMPortsTable(&out, results); err != nil {
|
||||
if err := printVMPortsTable(&out, result); err != nil {
|
||||
t.Fatalf("printVMPortsTable: %v", err)
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
|
||||
if len(lines) != 3 {
|
||||
t.Fatalf("lines = %q, want header + 2 rows", lines)
|
||||
}
|
||||
if !strings.Contains(lines[0], "VM") || !strings.Contains(lines[0], "WEB") {
|
||||
t.Fatalf("header = %q, want VM/WEB columns", lines[0])
|
||||
if !strings.Contains(lines[0], "PROTO") || !strings.Contains(lines[0], "ENDPOINT") || strings.Contains(lines[0], "VM") || strings.Contains(lines[0], "WEB") {
|
||||
t.Fatalf("header = %q, want PROTO/ENDPOINT without VM/WEB", lines[0])
|
||||
}
|
||||
if !strings.Contains(lines[1], "alpha") || !strings.Contains(lines[1], "alpha.vm:53") || !strings.Contains(lines[1], "\t-\n") {
|
||||
// tabwriter output is space-expanded, so just require the dash placeholder.
|
||||
if !strings.Contains(lines[1], "alpha") || !strings.Contains(lines[1], "alpha.vm:53") || !strings.HasSuffix(strings.TrimSpace(lines[1]), "-") {
|
||||
t.Fatalf("first row = %q, want alpha row with dash web column", lines[1])
|
||||
}
|
||||
if !strings.Contains(lines[1], "https") || !strings.Contains(lines[1], "https://alpha.vm:443/") {
|
||||
t.Fatalf("first row = %q, want https endpoint row", lines[1])
|
||||
}
|
||||
if !strings.Contains(lines[2], "beta") || !strings.Contains(lines[2], "http://beta.vm:8080/") {
|
||||
t.Fatalf("second row = %q, want beta web url", lines[2])
|
||||
if !strings.Contains(lines[2], "udp") || !strings.Contains(lines[2], "alpha.vm:53") {
|
||||
t.Fatalf("second row = %q, want udp endpoint row", lines[2])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue