diff --git a/cmd/cloudflared/tunnel/subcommands.go b/cmd/cloudflared/tunnel/subcommands.go index 9a6bb517..306ce13b 100644 --- a/cmd/cloudflared/tunnel/subcommands.go +++ b/cmd/cloudflared/tunnel/subcommands.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "os" + "sort" + "strings" "time" "github.com/pkg/errors" @@ -97,10 +99,10 @@ func listTunnels(c *cli.Context) error { return renderOutput(outputFormat, tunnels) } if len(tunnels) > 0 { - const listFormat = "%-40s%-40s%s\n" - fmt.Printf(listFormat, "ID", "NAME", "CREATED") + const listFormat = "%-40s%-30s%-30s%s\n" + fmt.Printf(listFormat, "ID", "NAME", "CREATED", "CONNECTIONS") for _, t := range tunnels { - fmt.Printf(listFormat, t.ID, t.Name, t.CreatedAt.Format(time.RFC3339)) + fmt.Printf(listFormat, t.ID, t.Name, t.CreatedAt.Format(time.RFC3339), fmtConnections(t.Connections)) } } else { fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel") @@ -109,6 +111,29 @@ func listTunnels(c *cli.Context) error { return nil } +func fmtConnections(connections []tunnelstore.Connection) string { + + // Count connections per colo + numConnsPerColo := make(map[string]uint, len(connections)) + for _, connection := range connections { + numConnsPerColo[connection.ColoName]++ + } + + // Get sorted list of colos + sortedColos := []string{} + for coloName := range numConnsPerColo { + sortedColos = append(sortedColos, coloName) + } + sort.Strings(sortedColos) + + // Map each colo to its frequency, combine into output string. + var output []string + for _, coloName := range sortedColos { + output = append(output, fmt.Sprintf("%dx%s", numConnsPerColo[coloName], coloName)) + } + return strings.Join(output, ", ") +} + func buildDeleteCommand() *cli.Command { return &cli.Command{ Name: "delete", diff --git a/cmd/cloudflared/tunnel/subcommands_test.go b/cmd/cloudflared/tunnel/subcommands_test.go new file mode 100644 index 00000000..202aeeb4 --- /dev/null +++ b/cmd/cloudflared/tunnel/subcommands_test.go @@ -0,0 +1,71 @@ +package tunnel + +import ( + "testing" + + "github.com/cloudflare/cloudflared/tunnelstore" + + "github.com/google/uuid" +) + +func Test_fmtConnections(t *testing.T) { + type args struct { + connections []tunnelstore.Connection + } + tests := []struct { + name string + args args + want string + }{ + { + name: "empty", + args: args{ + connections: []tunnelstore.Connection{}, + }, + want: "", + }, + { + name: "trivial", + args: args{ + connections: []tunnelstore.Connection{ + { + ColoName: "DFW", + ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"), + }, + }, + }, + want: "1xDFW", + }, + { + name: "many colos", + args: args{ + connections: []tunnelstore.Connection{ + { + ColoName: "YRV", + ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"), + }, + { + ColoName: "DFW", + ID: uuid.MustParse("c13c0b3b-0fbf-453c-8169-a1990fced6d0"), + }, + { + ColoName: "ATL", + ID: uuid.MustParse("70c90639-e386-4e8d-9a4e-7f046d70e63f"), + }, + { + ColoName: "DFW", + ID: uuid.MustParse("30ad6251-0305-4635-a670-d3994f474981"), + }, + }, + }, + want: "1xATL, 2xDFW, 1xYRV", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := fmtConnections(tt.args.connections); got != tt.want { + t.Errorf("fmtConnections() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/tunnelstore/client.go b/tunnelstore/client.go index fbddee8a..58d332f8 100644 --- a/tunnelstore/client.go +++ b/tunnelstore/client.go @@ -10,6 +10,7 @@ import ( "time" "github.com/cloudflare/cloudflared/logger" + "github.com/google/uuid" "github.com/pkg/errors" ) @@ -26,9 +27,15 @@ var ( ) type Tunnel struct { - ID string `json:"id"` - Name string `json:"name"` - CreatedAt time.Time `json:"created_at"` + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + Connections []Connection `json:"connections"` +} + +type Connection struct { + ColoName string `json:"colo_name"` + ID uuid.UUID `json:"uuid"` } type Client interface {