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
12
README.md
12
README.md
|
|
@ -110,9 +110,9 @@ banger vm ssh calm-otter
|
||||||
When the SSH session exits normally, `banger` checks the guest over vsock and
|
When the SSH session exits normally, `banger` checks the guest over vsock and
|
||||||
reminds you if the VM is still running.
|
reminds you if the VM is still running.
|
||||||
|
|
||||||
Inspect host-reachable listening ports for one or more running VMs:
|
Inspect host-reachable listening ports for a running VM:
|
||||||
```bash
|
```bash
|
||||||
banger vm ports calm-otter buildbox
|
banger vm ports calm-otter
|
||||||
```
|
```
|
||||||
|
|
||||||
Stop, restart, kill, or delete it:
|
Stop, restart, kill, or delete it:
|
||||||
|
|
@ -252,10 +252,10 @@ guest IPv4 address. Integrate your local resolver separately if you want
|
||||||
transparent `.vm` lookups on the host.
|
transparent `.vm` lookups on the host.
|
||||||
|
|
||||||
`banger vm ports` asks the guest-side `banger-vsock-agent` to run `ss`, then
|
`banger vm ports` asks the guest-side `banger-vsock-agent` to run `ss`, then
|
||||||
prints host-usable `<hostname>.vm:port` endpoints plus the owning
|
prints host-usable endpoints plus the owning process/command. TCP listeners get
|
||||||
process/command. TCP listeners get a short best-effort HTTP probe; when the
|
short best-effort HTTP and HTTPS probes; detected web listeners are shown as
|
||||||
probe sees a real HTTP response, the command includes a clickable
|
`http` or `https`, and the endpoint column becomes a clickable URL such as
|
||||||
`http://<hostname>.vm:port/` URL. Older images without `ss` may need rebuilding
|
`https://<hostname>.vm:port/`. Older images without `ss` may need rebuilding
|
||||||
before `vm ports` works.
|
before `vm ports` works.
|
||||||
|
|
||||||
## Storage Model
|
## Storage Model
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,6 @@ type VMPort struct {
|
||||||
Process string `json:"process,omitempty"`
|
Process string `json:"process,omitempty"`
|
||||||
Command string `json:"command,omitempty"`
|
Command string `json:"command,omitempty"`
|
||||||
Endpoint string `json:"endpoint,omitempty"`
|
Endpoint string `json:"endpoint,omitempty"`
|
||||||
WebURL string `json:"web_url,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type VMPortsResult struct {
|
type VMPortsResult struct {
|
||||||
|
|
|
||||||
|
|
@ -549,44 +549,19 @@ func newVMStatsCommand() *cobra.Command {
|
||||||
|
|
||||||
func newVMPortsCommand() *cobra.Command {
|
func newVMPortsCommand() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "ports <id-or-name>...",
|
Use: "ports <id-or-name>",
|
||||||
Short: "Show host-reachable listening guest ports",
|
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 {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
layout, _, err := ensureDaemon(cmd.Context())
|
layout, _, err := ensureDaemon(cmd.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
targets, resolutionErrs := resolveVMTargets(listResult.VMs, args)
|
return printVMPortsTable(cmd.OutOrStdout(), result)
|
||||||
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
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -793,12 +768,6 @@ type vmBatchActionResult struct {
|
||||||
Err error
|
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 {
|
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{})
|
listResult, err := rpc.Call[api.VMListResult](cmd.Context(), socketPath, "vm.list", api.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -907,27 +876,6 @@ func executeVMActionBatch(ctx context.Context, targets []resolvedVMTarget, actio
|
||||||
return results
|
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) {
|
func ensureDaemon(ctx context.Context) (paths.Layout, model.DaemonConfig, error) {
|
||||||
layout, err := paths.Resolve()
|
layout, err := paths.Resolve()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1223,41 +1171,25 @@ func printImageSummary(out anyWriter, image model.Image) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func printVMPortsTable(out anyWriter, results []vmPortsBatchResult) error {
|
func printVMPortsTable(out anyWriter, result api.VMPortsResult) error {
|
||||||
type portRow struct {
|
type portRow struct {
|
||||||
VM string
|
|
||||||
Proto string
|
Proto string
|
||||||
Endpoint string
|
Endpoint string
|
||||||
Process string
|
Process string
|
||||||
Command string
|
Command string
|
||||||
WebURL string
|
|
||||||
Port int
|
Port int
|
||||||
}
|
}
|
||||||
rows := make([]portRow, 0)
|
rows := make([]portRow, 0, len(result.Ports))
|
||||||
for _, result := range results {
|
for _, port := range result.Ports {
|
||||||
if result.Err != nil {
|
rows = append(rows, portRow{
|
||||||
continue
|
Proto: port.Proto,
|
||||||
}
|
Endpoint: port.Endpoint,
|
||||||
vmName := strings.TrimSpace(result.Result.Name)
|
Process: port.Process,
|
||||||
if vmName == "" {
|
Command: port.Command,
|
||||||
vmName = result.Target.VM.Name
|
Port: port.Port,
|
||||||
}
|
})
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sort.Slice(rows, func(i, j int) bool {
|
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 {
|
if rows[i].Proto != rows[j].Proto {
|
||||||
return 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)
|
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
|
return err
|
||||||
}
|
}
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
if _, err := fmt.Fprintf(
|
if _, err := fmt.Fprintf(
|
||||||
w,
|
w,
|
||||||
"%s\t%s\t%s\t%s\t%s\t%s\n",
|
"%s\t%s\t%s\t%s\n",
|
||||||
row.VM,
|
|
||||||
row.Proto,
|
row.Proto,
|
||||||
emptyDash(row.Endpoint),
|
emptyDash(row.Endpoint),
|
||||||
emptyDash(row.Process),
|
emptyDash(row.Process),
|
||||||
emptyDash(row.Command),
|
emptyDash(row.Command),
|
||||||
row.WebURL,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
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) {
|
func TestVMSetParamsFromFlags(t *testing.T) {
|
||||||
params, err := vmSetParamsFromFlags("devbox", 4, 2048, "16G", true, false)
|
params, err := vmSetParamsFromFlags("devbox", 4, 2048, "16G", true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -279,56 +288,43 @@ func TestAbsolutizeImageRegisterPaths(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrintVMPortsTableSortsAndRendersURLs(t *testing.T) {
|
func TestPrintVMPortsTableSortsAndRendersURLEndpoints(t *testing.T) {
|
||||||
results := []vmPortsBatchResult{
|
result := api.VMPortsResult{
|
||||||
{
|
Name: "alpha",
|
||||||
Target: resolvedVMTarget{Ref: "beta"},
|
Ports: []api.VMPort{
|
||||||
Result: api.VMPortsResult{
|
{
|
||||||
Name: "beta",
|
Proto: "https",
|
||||||
Ports: []api.VMPort{{
|
Port: 443,
|
||||||
Proto: "tcp",
|
Endpoint: "https://alpha.vm:443/",
|
||||||
Port: 8080,
|
Process: "caddy",
|
||||||
Endpoint: "beta.vm:8080",
|
Command: "caddy run",
|
||||||
Process: "python3",
|
|
||||||
Command: "python3 -m http.server 8080",
|
|
||||||
WebURL: "http://beta.vm:8080/",
|
|
||||||
}},
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
Proto: "udp",
|
||||||
Target: resolvedVMTarget{Ref: "alpha"},
|
Port: 53,
|
||||||
Result: api.VMPortsResult{
|
Endpoint: "alpha.vm:53",
|
||||||
Name: "alpha",
|
Process: "dnsd",
|
||||||
Ports: []api.VMPort{{
|
Command: "dnsd --foreground",
|
||||||
Proto: "udp",
|
|
||||||
Port: 53,
|
|
||||||
Endpoint: "alpha.vm:53",
|
|
||||||
Process: "dnsd",
|
|
||||||
Command: "dnsd --foreground",
|
|
||||||
}},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
if err := printVMPortsTable(&out, results); err != nil {
|
if err := printVMPortsTable(&out, result); err != nil {
|
||||||
t.Fatalf("printVMPortsTable: %v", err)
|
t.Fatalf("printVMPortsTable: %v", err)
|
||||||
}
|
}
|
||||||
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
|
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
|
||||||
if len(lines) != 3 {
|
if len(lines) != 3 {
|
||||||
t.Fatalf("lines = %q, want header + 2 rows", lines)
|
t.Fatalf("lines = %q, want header + 2 rows", lines)
|
||||||
}
|
}
|
||||||
if !strings.Contains(lines[0], "VM") || !strings.Contains(lines[0], "WEB") {
|
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 VM/WEB columns", lines[0])
|
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") {
|
if !strings.Contains(lines[1], "https") || !strings.Contains(lines[1], "https://alpha.vm:443/") {
|
||||||
// tabwriter output is space-expanded, so just require the dash placeholder.
|
t.Fatalf("first row = %q, want https endpoint row", lines[1])
|
||||||
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[2], "beta") || !strings.Contains(lines[2], "http://beta.vm:8080/") {
|
if !strings.Contains(lines[2], "udp") || !strings.Contains(lines[2], "alpha.vm:53") {
|
||||||
t.Fatalf("second row = %q, want beta web url", lines[2])
|
t.Fatalf("second row = %q, want udp endpoint row", lines[2])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package daemon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -78,8 +79,11 @@ func buildVMPorts(vm model.VMRecord, listeners []vsockagent.PortListener) []api.
|
||||||
if port.Command == "" {
|
if port.Command == "" {
|
||||||
port.Command = port.Process
|
port.Command = port.Process
|
||||||
}
|
}
|
||||||
if port.Proto == "tcp" && probeHost != "" && endpointHost != "" && probeHTTPListener(probeHost, listener.Port) {
|
if port.Proto == "tcp" && probeHost != "" && endpointHost != "" {
|
||||||
port.WebURL = "http://" + net.JoinHostPort(endpointHost, strconv.Itoa(listener.Port)) + "/"
|
if scheme, ok := probeWebListener(probeHost, listener.Port); ok {
|
||||||
|
port.Proto = scheme
|
||||||
|
port.Endpoint = scheme + "://" + net.JoinHostPort(endpointHost, strconv.Itoa(listener.Port)) + "/"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ports = append(ports, port)
|
ports = append(ports, port)
|
||||||
}
|
}
|
||||||
|
|
@ -90,31 +94,49 @@ func buildVMPorts(vm model.VMRecord, listeners []vsockagent.PortListener) []api.
|
||||||
if ports[i].Port != ports[j].Port {
|
if ports[i].Port != ports[j].Port {
|
||||||
return ports[i].Port < ports[j].Port
|
return ports[i].Port < ports[j].Port
|
||||||
}
|
}
|
||||||
|
if ports[i].PID != ports[j].PID {
|
||||||
|
return ports[i].PID < ports[j].PID
|
||||||
|
}
|
||||||
if ports[i].Process != ports[j].Process {
|
if ports[i].Process != ports[j].Process {
|
||||||
return ports[i].Process < ports[j].Process
|
return ports[i].Process < ports[j].Process
|
||||||
}
|
}
|
||||||
return ports[i].Command < ports[j].Command
|
if ports[i].Command != ports[j].Command {
|
||||||
|
return ports[i].Command < ports[j].Command
|
||||||
|
}
|
||||||
|
return ports[i].BindAddress < ports[j].BindAddress
|
||||||
})
|
})
|
||||||
return ports
|
return dedupeVMPorts(ports)
|
||||||
}
|
}
|
||||||
|
|
||||||
func probeHTTPListener(guestIP string, port int) bool {
|
func probeWebListener(guestIP string, port int) (string, bool) {
|
||||||
|
if probeHTTPScheme("https", guestIP, port) {
|
||||||
|
return "https", true
|
||||||
|
}
|
||||||
|
if probeHTTPScheme("http", guestIP, port) {
|
||||||
|
return "http", true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func probeHTTPScheme(scheme, guestIP string, port int) bool {
|
||||||
if strings.TrimSpace(guestIP) == "" || port <= 0 {
|
if strings.TrimSpace(guestIP) == "" || port <= 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
url := "http://" + net.JoinHostPort(strings.TrimSpace(guestIP), strconv.Itoa(port)) + "/"
|
url := scheme + "://" + net.JoinHostPort(strings.TrimSpace(guestIP), strconv.Itoa(port)) + "/"
|
||||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
transport := &http.Transport{Proxy: nil}
|
||||||
|
if scheme == "https" {
|
||||||
|
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||||
|
}
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: httpProbeTimeout,
|
Timeout: httpProbeTimeout,
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
},
|
},
|
||||||
Transport: &http.Transport{
|
Transport: transport,
|
||||||
Proxy: nil,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -124,3 +146,20 @@ func probeHTTPListener(guestIP string, port int) bool {
|
||||||
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1))
|
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1))
|
||||||
return resp.ProtoMajor >= 1
|
return resp.ProtoMajor >= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dedupeVMPorts(ports []api.VMPort) []api.VMPort {
|
||||||
|
if len(ports) < 2 {
|
||||||
|
return ports
|
||||||
|
}
|
||||||
|
deduped := make([]api.VMPort, 0, len(ports))
|
||||||
|
seen := make(map[string]struct{}, len(ports))
|
||||||
|
for _, port := range ports {
|
||||||
|
key := port.Proto + "\x00" + port.Endpoint
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
deduped = append(deduped, port)
|
||||||
|
}
|
||||||
|
return deduped
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"banger/internal/paths"
|
"banger/internal/paths"
|
||||||
"banger/internal/store"
|
"banger/internal/store"
|
||||||
"banger/internal/vmdns"
|
"banger/internal/vmdns"
|
||||||
|
"banger/internal/vsockagent"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFindVMPrefixResolution(t *testing.T) {
|
func TestFindVMPrefixResolution(t *testing.T) {
|
||||||
|
|
@ -428,7 +429,7 @@ func TestHealthVMReturnsFalseForStoppedVM(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPortsVMReturnsEnrichedPortsAndWebURL(t *testing.T) {
|
func TestPortsVMReturnsEnrichedPortsAndWebSchemes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
@ -448,6 +449,20 @@ func TestPortsVMReturnsEnrichedPortsAndWebURL(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ResolveTCPAddr: %v", err)
|
t.Fatalf("ResolveTCPAddr: %v", err)
|
||||||
}
|
}
|
||||||
|
tlsServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
}))
|
||||||
|
tlsListener, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listen tls: %v", err)
|
||||||
|
}
|
||||||
|
tlsServer.Listener = tlsListener
|
||||||
|
tlsServer.StartTLS()
|
||||||
|
t.Cleanup(tlsServer.Close)
|
||||||
|
tlsAddr, err := net.ResolveTCPAddr("tcp", strings.TrimPrefix(tlsServer.URL, "https://"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveTCPAddr(tls): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
vsockSock := filepath.Join(t.TempDir(), "fc.vsock")
|
vsockSock := filepath.Join(t.TempDir(), "fc.vsock")
|
||||||
listener, err := net.Listen("unix", vsockSock)
|
listener, err := net.Listen("unix", vsockSock)
|
||||||
|
|
@ -496,7 +511,7 @@ func TestPortsVMReturnsEnrichedPortsAndWebURL(t *testing.T) {
|
||||||
serverDone <- fmt.Errorf("unexpected ports payload %q", got)
|
serverDone <- fmt.Errorf("unexpected ports payload %q", got)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
body := fmt.Sprintf(`{"listeners":[{"proto":"tcp","bind_address":"0.0.0.0","port":%d,"pid":44,"process":"python3","command":"python3 -m http.server %d"},{"proto":"udp","bind_address":"0.0.0.0","port":53,"pid":1,"process":"dnsd","command":"dnsd --foreground"}]}`, webAddr.Port, webAddr.Port)
|
body := fmt.Sprintf(`{"listeners":[{"proto":"tcp","bind_address":"0.0.0.0","port":%d,"pid":44,"process":"python3","command":"python3 -m http.server %d"},{"proto":"tcp","bind_address":"0.0.0.0","port":%d,"pid":77,"process":"caddy","command":"caddy run"},{"proto":"udp","bind_address":"0.0.0.0","port":53,"pid":1,"process":"dnsd","command":"dnsd --foreground"}]}`, webAddr.Port, webAddr.Port, tlsAddr.Port)
|
||||||
resp := fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s", len(body), body)
|
resp := fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s", len(body), body)
|
||||||
_, err = conn.Write([]byte(resp))
|
_, err = conn.Write([]byte(resp))
|
||||||
serverDone <- err
|
serverDone <- err
|
||||||
|
|
@ -527,24 +542,30 @@ func TestPortsVMReturnsEnrichedPortsAndWebURL(t *testing.T) {
|
||||||
if result.Name != vm.Name || result.DNSName != vm.Runtime.DNSName {
|
if result.Name != vm.Name || result.DNSName != vm.Runtime.DNSName {
|
||||||
t.Fatalf("result = %+v, want name/dns", result)
|
t.Fatalf("result = %+v, want name/dns", result)
|
||||||
}
|
}
|
||||||
if len(result.Ports) != 2 {
|
if len(result.Ports) != 3 {
|
||||||
t.Fatalf("ports = %+v, want 2 entries", result.Ports)
|
t.Fatalf("ports = %+v, want 3 entries", result.Ports)
|
||||||
}
|
}
|
||||||
wantWeb := fmt.Sprintf("http://ports.vm:%d/", webAddr.Port)
|
wantHTTP := fmt.Sprintf("http://ports.vm:%d/", webAddr.Port)
|
||||||
var tcpPort, udpPort api.VMPort
|
wantHTTPS := fmt.Sprintf("https://ports.vm:%d/", tlsAddr.Port)
|
||||||
|
var httpPort, httpsPort, udpPort api.VMPort
|
||||||
for _, port := range result.Ports {
|
for _, port := range result.Ports {
|
||||||
switch port.Proto {
|
switch port.Port {
|
||||||
case "tcp":
|
case webAddr.Port:
|
||||||
tcpPort = port
|
httpPort = port
|
||||||
case "udp":
|
case tlsAddr.Port:
|
||||||
|
httpsPort = port
|
||||||
|
case 53:
|
||||||
udpPort = port
|
udpPort = port
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if udpPort.Endpoint != "ports.vm:53" || udpPort.WebURL != "" {
|
if udpPort.Endpoint != "ports.vm:53" {
|
||||||
t.Fatalf("udp port = %+v, want endpoint only", udpPort)
|
t.Fatalf("udp port = %+v, want endpoint only", udpPort)
|
||||||
}
|
}
|
||||||
if tcpPort.Endpoint != net.JoinHostPort("ports.vm", strconv.Itoa(webAddr.Port)) || tcpPort.WebURL != wantWeb {
|
if httpPort.Proto != "http" || httpPort.Endpoint != wantHTTP {
|
||||||
t.Fatalf("tcp port = %+v, want web url %q", tcpPort, wantWeb)
|
t.Fatalf("http port = %+v, want http endpoint %q", httpPort, wantHTTP)
|
||||||
|
}
|
||||||
|
if httpsPort.Proto != "https" || httpsPort.Endpoint != wantHTTPS {
|
||||||
|
t.Fatalf("https port = %+v, want https endpoint %q", httpsPort, wantHTTPS)
|
||||||
}
|
}
|
||||||
runner.assertExhausted()
|
runner.assertExhausted()
|
||||||
if err := <-serverDone; err != nil {
|
if err := <-serverDone; err != nil {
|
||||||
|
|
@ -567,6 +588,49 @@ func TestPortsVMReturnsErrorForStoppedVM(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildVMPortsDeduplicatesSameRenderedEndpoint(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
vm := testVM("dedupe-ports", "image-ports", "")
|
||||||
|
vm.Runtime.DNSName = "dedupe-ports.vm"
|
||||||
|
|
||||||
|
ports := buildVMPorts(vm, []vsockagent.PortListener{
|
||||||
|
{
|
||||||
|
Proto: "tcp",
|
||||||
|
BindAddress: "0.0.0.0",
|
||||||
|
Port: 8080,
|
||||||
|
PID: 44,
|
||||||
|
Process: "docker-proxy",
|
||||||
|
Command: "/usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Proto: "tcp",
|
||||||
|
BindAddress: "::",
|
||||||
|
Port: 8080,
|
||||||
|
PID: 45,
|
||||||
|
Process: "docker-proxy",
|
||||||
|
Command: "/usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 8080",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Proto: "udp",
|
||||||
|
BindAddress: "0.0.0.0",
|
||||||
|
Port: 8080,
|
||||||
|
PID: 46,
|
||||||
|
Process: "dnsd",
|
||||||
|
Command: "dnsd --foreground",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if len(ports) != 2 {
|
||||||
|
t.Fatalf("ports = %+v, want tcp+udp entries after dedupe", ports)
|
||||||
|
}
|
||||||
|
if ports[0].Proto != "tcp" || ports[0].Endpoint != "dedupe-ports.vm:8080" {
|
||||||
|
t.Fatalf("first port = %+v, want deduped tcp endpoint", ports[0])
|
||||||
|
}
|
||||||
|
if ports[1].Proto != "udp" || ports[1].Endpoint != "dedupe-ports.vm:8080" {
|
||||||
|
t.Fatalf("second port = %+v, want distinct udp endpoint", ports[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSetVMDiskResizeFailsPreflightWhenToolsMissing(t *testing.T) {
|
func TestSetVMDiskResizeFailsPreflightWhenToolsMissing(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
db := openDaemonStore(t)
|
db := openDaemonStore(t)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue