@@ -29,9 +29,10 @@ import (
2929)
3030
3131const (
32- allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
33- CredFileFlagAlias = "cred-file"
34- CredFileFlag = "credentials-file"
32+ allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
33+ connsSortByOptions = "id, startedAt, numConnections, version"
34+ CredFileFlagAlias = "cred-file"
35+ CredFileFlag = "credentials-file"
3536
3637 LogFieldTunnelID = "tunnelID"
3738)
@@ -64,11 +65,11 @@ var (
6465 Aliases : []string {"rd" },
6566 Usage : "Include connections that have recently disconnected in the list" ,
6667 }
67- outputFormatFlag = altsrc . NewStringFlag ( & cli.StringFlag {
68+ outputFormatFlag = & cli.StringFlag {
6869 Name : "output" ,
6970 Aliases : []string {"o" },
7071 Usage : "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'" ,
71- })
72+ }
7273 sortByFlag = & cli.StringFlag {
7374 Name : "sort-by" ,
7475 Value : "name" ,
@@ -114,6 +115,17 @@ var (
114115 EnvVars : []string {"TUNNEL_TRANSPORT_PROTOCOL" },
115116 Hidden : true ,
116117 })
118+ sortInfoByFlag = & cli.StringFlag {
119+ Name : "sort-by" ,
120+ Value : "createdAt" ,
121+ Usage : fmt .Sprintf ("Sorts the list of connections of a tunnel by the given field. Valid options are {%s}" , connsSortByOptions ),
122+ EnvVars : []string {"TUNNEL_INFO_SORT_BY" },
123+ }
124+ invertInfoSortFlag = & cli.BoolFlag {
125+ Name : "invert-sort" ,
126+ Usage : "Inverts the sort order of the tunnel info." ,
127+ EnvVars : []string {"TUNNEL_INFO_INVERT_SORT" },
128+ }
117129)
118130
119131func buildCreateCommand () * cli.Command {
@@ -214,6 +226,9 @@ func listCommand(c *cli.Context) error {
214226 return err
215227 }
216228
229+ warningChecker := updater .StartWarningCheck (c )
230+ defer warningChecker .LogWarningIfAny (sc .log )
231+
217232 filter := tunnelstore .NewFilter ()
218233 if ! c .Bool ("show-deleted" ) {
219234 filter .NoDeleted ()
@@ -232,9 +247,6 @@ func listCommand(c *cli.Context) error {
232247 filter .ByTunnelID (tunnelID )
233248 }
234249
235- warningChecker := updater .StartWarningCheck (c )
236- defer warningChecker .LogWarningIfAny (sc .log )
237-
238250 tunnels , err := sc .list (filter )
239251 if err != nil {
240252 return err
@@ -284,17 +296,11 @@ func listCommand(c *cli.Context) error {
284296}
285297
286298func formatAndPrintTunnelList (tunnels []* tunnelstore.Tunnel , showRecentlyDisconnected bool ) {
287- const (
288- minWidth = 0
289- tabWidth = 8
290- padding = 1
291- padChar = ' '
292- flags = 0
293- )
294-
295- writer := tabwriter .NewWriter (os .Stdout , minWidth , tabWidth , padding , padChar , flags )
299+ writer := tabWriter ()
296300 defer writer .Flush ()
297301
302+ _ , _ = fmt .Fprintln (writer , "You can obtain more detailed information for each tunnel with `cloudflared tunnel info <name/uuid>`" )
303+
298304 // Print column headers with tabbed columns
299305 _ , _ = fmt .Fprintln (writer , "ID\t NAME\t CREATED\t CONNECTIONS\t " )
300306
@@ -336,6 +342,152 @@ func fmtConnections(connections []tunnelstore.Connection, showRecentlyDisconnect
336342 return strings .Join (output , ", " )
337343}
338344
345+ func buildInfoCommand () * cli.Command {
346+ return & cli.Command {
347+ Name : "info" ,
348+ Action : cliutil .ConfiguredAction (tunnelInfo ),
349+ Usage : "List details about the active connectors for a tunnel" ,
350+ UsageText : "cloudflared tunnel [tunnel command options] info [subcommand options] [TUNNEL]" ,
351+ Description : "cloudflared tunnel info displays details about the active connectors for a given tunnel (identified by name or uuid)." ,
352+ Flags : []cli.Flag {
353+ outputFormatFlag ,
354+ showRecentlyDisconnected ,
355+ sortInfoByFlag ,
356+ invertInfoSortFlag ,
357+ },
358+ CustomHelpTemplate : commandHelpTemplate (),
359+ }
360+ }
361+
362+ func tunnelInfo (c * cli.Context ) error {
363+ sc , err := newSubcommandContext (c )
364+ if err != nil {
365+ return err
366+ }
367+
368+ warningChecker := updater .StartWarningCheck (c )
369+ defer warningChecker .LogWarningIfAny (sc .log )
370+
371+ if c .NArg () > 1 {
372+ return cliutil .UsageError (`"cloudflared tunnel info" accepts only one argument, the ID or name of the tunnel to run.` )
373+ }
374+ tunnelID , err := sc .findID (c .Args ().First ())
375+ if err != nil {
376+ return errors .Wrap (err , "error parsing tunnel ID" )
377+ }
378+
379+ client , err := sc .client ()
380+ if err != nil {
381+ return err
382+ }
383+
384+ clients , err := client .ListActiveClients (tunnelID )
385+ if err != nil {
386+ return err
387+ }
388+
389+ sortBy := c .String ("sort-by" )
390+ invalidSortField := false
391+ sort .Slice (clients , func (i , j int ) bool {
392+ cmp := func () bool {
393+ switch sortBy {
394+ case "id" :
395+ return clients [i ].ID .String () < clients [j ].ID .String ()
396+ case "createdAt" :
397+ return clients [i ].RunAt .Unix () < clients [j ].RunAt .Unix ()
398+ case "numConnections" :
399+ return len (clients [i ].Connections ) < len (clients [j ].Connections )
400+ case "version" :
401+ return clients [i ].Version < clients [j ].Version
402+ default :
403+ invalidSortField = true
404+ return clients [i ].RunAt .Unix () < clients [j ].RunAt .Unix ()
405+ }
406+ }()
407+ if c .Bool ("invert-sort" ) {
408+ return ! cmp
409+ }
410+ return cmp
411+ })
412+ if invalidSortField {
413+ sc .log .Error ().Msgf ("%s is not a valid sort field. Valid sort fields are %s. Defaulting to 'name'." , sortBy , connsSortByOptions )
414+ }
415+
416+ tunnel , err := getTunnel (sc , tunnelID )
417+ if err != nil {
418+ return err
419+ }
420+ info := Info {
421+ tunnel .ID ,
422+ tunnel .Name ,
423+ tunnel .CreatedAt ,
424+ clients ,
425+ }
426+
427+ if outputFormat := c .String (outputFormatFlag .Name ); outputFormat != "" {
428+ return renderOutput (outputFormat , info )
429+ }
430+
431+ if len (clients ) > 0 {
432+ formatAndPrintConnectionsList (info , c .Bool ("show-recently-disconnected" ))
433+ } else {
434+ fmt .Printf ("Your tunnel %s does not have any active connection.\n " , tunnelID )
435+ }
436+
437+ return nil
438+ }
439+
440+ func getTunnel (sc * subcommandContext , tunnelID uuid.UUID ) (* tunnelstore.Tunnel , error ) {
441+ filter := tunnelstore .NewFilter ()
442+ filter .ByTunnelID (tunnelID )
443+ tunnels , err := sc .list (filter )
444+ if err != nil {
445+ return nil , err
446+ }
447+ if len (tunnels ) != 1 {
448+ return nil , errors .Errorf ("Expected to find a single tunnel with uuid %v but found %d tunnels." , tunnelID , len (tunnels ))
449+ }
450+ return tunnels [0 ], nil
451+ }
452+
453+ func formatAndPrintConnectionsList (tunnelInfo Info , showRecentlyDisconnected bool ) {
454+ writer := tabWriter ()
455+ defer writer .Flush ()
456+
457+ _ , _ = fmt .Fprintf (writer , "NAME: %s\n ID: %s\n CREATED: %s\n \n " , tunnelInfo .Name , tunnelInfo .ID , tunnelInfo .CreatedAt )
458+
459+ _ , _ = fmt .Fprintln (writer , "CONNECTOR ID\t CREATED\t ARCHITECTURE\t VERSION\t ORIGIN IP\t EDGE\t " )
460+ for _ , c := range tunnelInfo .Connectors {
461+ var originIp = ""
462+ if len (c .Connections ) > 0 {
463+ originIp = c .Connections [0 ].OriginIP .String ()
464+ }
465+ formattedStr := fmt .Sprintf (
466+ "%s\t %s\t %s\t %s\t %s\t %s\t " ,
467+ c .ID ,
468+ c .RunAt .Format (time .RFC3339 ),
469+ c .Arch ,
470+ c .Version ,
471+ originIp ,
472+ fmtConnections (c .Connections , showRecentlyDisconnected ),
473+ )
474+ _ , _ = fmt .Fprintln (writer , formattedStr )
475+ }
476+ }
477+
478+ func tabWriter () * tabwriter.Writer {
479+ const (
480+ minWidth = 0
481+ tabWidth = 8
482+ padding = 1
483+ padChar = ' '
484+ flags = 0
485+ )
486+
487+ writer := tabwriter .NewWriter (os .Stdout , minWidth , tabWidth , padding , padChar , flags )
488+ return writer
489+ }
490+
339491func buildDeleteCommand () * cli.Command {
340492 return & cli.Command {
341493 Name : "delete" ,
0 commit comments