TUN-3993: New `cloudflared tunnel info` to obtain details about the active connectors for a tunnel
This commit is contained in:
parent
a34099724e
commit
89d0e45d62
|
@ -8,11 +8,14 @@
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
- none
|
- It is now possible to obtain more detailed information about the cloudflared connectors to Cloudflare Edge via
|
||||||
|
`cloudflared tunnel info <name/uuid>`. It is possible to sort the output as well as output in different formats,
|
||||||
|
such as: `cloudflared tunnel info --sort-by version --invert-sort --output json <name/uuid>`.
|
||||||
|
You can obtain more information via `cloudflared tunnel info --help`.
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
||||||
- nonw
|
- none
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
|
|
@ -100,6 +100,7 @@ func Commands() []*cli.Command {
|
||||||
buildRouteCommand(),
|
buildRouteCommand(),
|
||||||
buildRunCommand(),
|
buildRunCommand(),
|
||||||
buildListCommand(),
|
buildListCommand(),
|
||||||
|
buildInfoCommand(),
|
||||||
buildIngressSubcommand(),
|
buildIngressSubcommand(),
|
||||||
buildDeleteCommand(),
|
buildDeleteCommand(),
|
||||||
buildCleanupCommand(),
|
buildCleanupCommand(),
|
||||||
|
@ -464,7 +465,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||||
credentialsFileFlag,
|
credentialsFileFlag,
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||||
Name: "is-autoupdated",
|
Name: "is-autoupdated",
|
||||||
Usage: "Signal the new process that Argo Tunnel client has been autoupdated",
|
Usage: "Signal the new process that Argo Tunnel connector has been autoupdated",
|
||||||
Value: false,
|
Value: false,
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -199,9 +199,9 @@ func prepareTunnelConfig(
|
||||||
if isNamedTunnel {
|
if isNamedTunnel {
|
||||||
clientUUID, err := uuid.NewRandom()
|
clientUUID, err := uuid.NewRandom()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ingress.Ingress{}, errors.Wrap(err, "can't generate clientUUID")
|
return nil, ingress.Ingress{}, errors.Wrap(err, "can't generate connector UUID")
|
||||||
}
|
}
|
||||||
log.Info().Msgf("Generated Client ID: %s", clientUUID)
|
log.Info().Msgf("Generated Connector ID: %s", clientUUID)
|
||||||
features := append(c.StringSlice("features"), origin.FeatureSerializedHeaders)
|
features := append(c.StringSlice("features"), origin.FeatureSerializedHeaders)
|
||||||
namedTunnel.Client = tunnelpogs.ClientInfo{
|
namedTunnel.Client = tunnelpogs.ClientInfo{
|
||||||
ClientID: clientUUID[:],
|
ClientID: clientUUID[:],
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package tunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Info struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
Connectors []*tunnelstore.ActiveClient `json:"conns"`
|
||||||
|
}
|
|
@ -343,6 +343,12 @@ func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
|
||||||
// one Tunnelstore API call.
|
// one Tunnelstore API call.
|
||||||
func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
|
func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
|
||||||
|
|
||||||
|
// Shortcut without Tunnelstore call if we find that all inputs are already UUIDs.
|
||||||
|
uuids, err := convertNamesToUuids(inputs, make(map[string]uuid.UUID))
|
||||||
|
if err == nil {
|
||||||
|
return uuids, nil
|
||||||
|
}
|
||||||
|
|
||||||
// First, look up all tunnels the user has
|
// First, look up all tunnels the user has
|
||||||
filter := tunnelstore.NewFilter()
|
filter := tunnelstore.NewFilter()
|
||||||
filter.NoDeleted()
|
filter.NoDeleted()
|
||||||
|
@ -362,7 +368,10 @@ func findIDs(tunnels []*tunnelstore.Tunnel, inputs []string) ([]uuid.UUID, error
|
||||||
nameToID[tunnel.Name] = tunnel.ID
|
nameToID[tunnel.Name] = tunnel.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each input, try to find the tunnel ID.
|
return convertNamesToUuids(inputs, nameToID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertNamesToUuids(inputs []string, nameToID map[string]uuid.UUID) ([]uuid.UUID, error) {
|
||||||
tunnelIDs := make([]uuid.UUID, len(inputs))
|
tunnelIDs := make([]uuid.UUID, len(inputs))
|
||||||
var badInputs []string
|
var badInputs []string
|
||||||
for i, input := range inputs {
|
for i, input := range inputs {
|
||||||
|
|
|
@ -29,9 +29,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
|
allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
|
||||||
CredFileFlagAlias = "cred-file"
|
connsSortByOptions = "id, startedAt, numConnections, version"
|
||||||
CredFileFlag = "credentials-file"
|
CredFileFlagAlias = "cred-file"
|
||||||
|
CredFileFlag = "credentials-file"
|
||||||
|
|
||||||
LogFieldTunnelID = "tunnelID"
|
LogFieldTunnelID = "tunnelID"
|
||||||
)
|
)
|
||||||
|
@ -64,11 +65,11 @@ var (
|
||||||
Aliases: []string{"rd"},
|
Aliases: []string{"rd"},
|
||||||
Usage: "Include connections that have recently disconnected in the list",
|
Usage: "Include connections that have recently disconnected in the list",
|
||||||
}
|
}
|
||||||
outputFormatFlag = altsrc.NewStringFlag(&cli.StringFlag{
|
outputFormatFlag = &cli.StringFlag{
|
||||||
Name: "output",
|
Name: "output",
|
||||||
Aliases: []string{"o"},
|
Aliases: []string{"o"},
|
||||||
Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'",
|
Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'",
|
||||||
})
|
}
|
||||||
sortByFlag = &cli.StringFlag{
|
sortByFlag = &cli.StringFlag{
|
||||||
Name: "sort-by",
|
Name: "sort-by",
|
||||||
Value: "name",
|
Value: "name",
|
||||||
|
@ -114,6 +115,17 @@ var (
|
||||||
EnvVars: []string{"TUNNEL_TRANSPORT_PROTOCOL"},
|
EnvVars: []string{"TUNNEL_TRANSPORT_PROTOCOL"},
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
})
|
})
|
||||||
|
sortInfoByFlag = &cli.StringFlag{
|
||||||
|
Name: "sort-by",
|
||||||
|
Value: "createdAt",
|
||||||
|
Usage: fmt.Sprintf("Sorts the list of connections of a tunnel by the given field. Valid options are {%s}", connsSortByOptions),
|
||||||
|
EnvVars: []string{"TUNNEL_INFO_SORT_BY"},
|
||||||
|
}
|
||||||
|
invertInfoSortFlag = &cli.BoolFlag{
|
||||||
|
Name: "invert-sort",
|
||||||
|
Usage: "Inverts the sort order of the tunnel info.",
|
||||||
|
EnvVars: []string{"TUNNEL_INFO_INVERT_SORT"},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildCreateCommand() *cli.Command {
|
func buildCreateCommand() *cli.Command {
|
||||||
|
@ -214,6 +226,9 @@ func listCommand(c *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
warningChecker := updater.StartWarningCheck(c)
|
||||||
|
defer warningChecker.LogWarningIfAny(sc.log)
|
||||||
|
|
||||||
filter := tunnelstore.NewFilter()
|
filter := tunnelstore.NewFilter()
|
||||||
if !c.Bool("show-deleted") {
|
if !c.Bool("show-deleted") {
|
||||||
filter.NoDeleted()
|
filter.NoDeleted()
|
||||||
|
@ -232,9 +247,6 @@ func listCommand(c *cli.Context) error {
|
||||||
filter.ByTunnelID(tunnelID)
|
filter.ByTunnelID(tunnelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
warningChecker := updater.StartWarningCheck(c)
|
|
||||||
defer warningChecker.LogWarningIfAny(sc.log)
|
|
||||||
|
|
||||||
tunnels, err := sc.list(filter)
|
tunnels, err := sc.list(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -284,17 +296,11 @@ func listCommand(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
|
func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
|
||||||
const (
|
writer := tabWriter()
|
||||||
minWidth = 0
|
|
||||||
tabWidth = 8
|
|
||||||
padding = 1
|
|
||||||
padChar = ' '
|
|
||||||
flags = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
|
|
||||||
defer writer.Flush()
|
defer writer.Flush()
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(writer, "You can obtain more detailed information for each tunnel with `cloudflared tunnel info <name/uuid>`")
|
||||||
|
|
||||||
// Print column headers with tabbed columns
|
// Print column headers with tabbed columns
|
||||||
_, _ = fmt.Fprintln(writer, "ID\tNAME\tCREATED\tCONNECTIONS\t")
|
_, _ = fmt.Fprintln(writer, "ID\tNAME\tCREATED\tCONNECTIONS\t")
|
||||||
|
|
||||||
|
@ -336,6 +342,152 @@ func fmtConnections(connections []tunnelstore.Connection, showRecentlyDisconnect
|
||||||
return strings.Join(output, ", ")
|
return strings.Join(output, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildInfoCommand() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "info",
|
||||||
|
Action: cliutil.ConfiguredAction(tunnelInfo),
|
||||||
|
Usage: "List details about the active connectors for a tunnel",
|
||||||
|
UsageText: "cloudflared tunnel [tunnel command options] info [subcommand options] [TUNNEL]",
|
||||||
|
Description: "cloudflared tunnel info displays details about the active connectors for a given tunnel (identified by name or uuid).",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
outputFormatFlag,
|
||||||
|
showRecentlyDisconnected,
|
||||||
|
sortInfoByFlag,
|
||||||
|
invertInfoSortFlag,
|
||||||
|
},
|
||||||
|
CustomHelpTemplate: commandHelpTemplate(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tunnelInfo(c *cli.Context) error {
|
||||||
|
sc, err := newSubcommandContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
warningChecker := updater.StartWarningCheck(c)
|
||||||
|
defer warningChecker.LogWarningIfAny(sc.log)
|
||||||
|
|
||||||
|
if c.NArg() > 1 {
|
||||||
|
return cliutil.UsageError(`"cloudflared tunnel info" accepts only one argument, the ID or name of the tunnel to run.`)
|
||||||
|
}
|
||||||
|
tunnelID, err := sc.findID(c.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error parsing tunnel ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := sc.client()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clients, err := client.ListActiveClients(tunnelID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sortBy := c.String("sort-by")
|
||||||
|
invalidSortField := false
|
||||||
|
sort.Slice(clients, func(i, j int) bool {
|
||||||
|
cmp := func() bool {
|
||||||
|
switch sortBy {
|
||||||
|
case "id":
|
||||||
|
return clients[i].ID.String() < clients[j].ID.String()
|
||||||
|
case "createdAt":
|
||||||
|
return clients[i].RunAt.Unix() < clients[j].RunAt.Unix()
|
||||||
|
case "numConnections":
|
||||||
|
return len(clients[i].Connections) < len(clients[j].Connections)
|
||||||
|
case "version":
|
||||||
|
return clients[i].Version < clients[j].Version
|
||||||
|
default:
|
||||||
|
invalidSortField = true
|
||||||
|
return clients[i].RunAt.Unix() < clients[j].RunAt.Unix()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if c.Bool("invert-sort") {
|
||||||
|
return !cmp
|
||||||
|
}
|
||||||
|
return cmp
|
||||||
|
})
|
||||||
|
if invalidSortField {
|
||||||
|
sc.log.Error().Msgf("%s is not a valid sort field. Valid sort fields are %s. Defaulting to 'name'.", sortBy, connsSortByOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel, err := getTunnel(sc, tunnelID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info := Info{
|
||||||
|
tunnel.ID,
|
||||||
|
tunnel.Name,
|
||||||
|
tunnel.CreatedAt,
|
||||||
|
clients,
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||||
|
return renderOutput(outputFormat, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(clients) > 0 {
|
||||||
|
formatAndPrintConnectionsList(info, c.Bool("show-recently-disconnected"))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Your tunnel %s does not have any active connection.\n", tunnelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) {
|
||||||
|
filter := tunnelstore.NewFilter()
|
||||||
|
filter.ByTunnelID(tunnelID)
|
||||||
|
tunnels, err := sc.list(filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(tunnels) != 1 {
|
||||||
|
return nil, errors.Errorf("Expected to find a single tunnel with uuid %v but found %d tunnels.", tunnelID, len(tunnels))
|
||||||
|
}
|
||||||
|
return tunnels[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAndPrintConnectionsList(tunnelInfo Info, showRecentlyDisconnected bool) {
|
||||||
|
writer := tabWriter()
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(writer, "NAME: %s\nID: %s\nCREATED: %s\n\n", tunnelInfo.Name, tunnelInfo.ID, tunnelInfo.CreatedAt)
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(writer, "CONNECTOR ID\tCREATED\tARCHITECTURE\tVERSION\tORIGIN IP\tEDGE\t")
|
||||||
|
for _, c := range tunnelInfo.Connectors {
|
||||||
|
var originIp = ""
|
||||||
|
if len(c.Connections) > 0 {
|
||||||
|
originIp = c.Connections[0].OriginIP.String()
|
||||||
|
}
|
||||||
|
formattedStr := fmt.Sprintf(
|
||||||
|
"%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||||
|
c.ID,
|
||||||
|
c.RunAt.Format(time.RFC3339),
|
||||||
|
c.Arch,
|
||||||
|
c.Version,
|
||||||
|
originIp,
|
||||||
|
fmtConnections(c.Connections, showRecentlyDisconnected),
|
||||||
|
)
|
||||||
|
_, _ = fmt.Fprintln(writer, formattedStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tabWriter() *tabwriter.Writer {
|
||||||
|
const (
|
||||||
|
minWidth = 0
|
||||||
|
tabWidth = 8
|
||||||
|
padding = 1
|
||||||
|
padChar = ' '
|
||||||
|
flags = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
|
||||||
|
return writer
|
||||||
|
}
|
||||||
|
|
||||||
func buildDeleteCommand() *cli.Command {
|
func buildDeleteCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "delete",
|
Name: "delete",
|
||||||
|
|
|
@ -43,6 +43,17 @@ type Connection struct {
|
||||||
ColoName string `json:"colo_name"`
|
ColoName string `json:"colo_name"`
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
IsPendingReconnect bool `json:"is_pending_reconnect"`
|
IsPendingReconnect bool `json:"is_pending_reconnect"`
|
||||||
|
OriginIP net.IP `json:"origin_ip"`
|
||||||
|
OpenedAt time.Time `json:"opened_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveClient struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Features []string `json:"features"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
RunAt time.Time `json:"run_at"`
|
||||||
|
Connections []Connection `json:"conns"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Change = string
|
type Change = string
|
||||||
|
@ -192,6 +203,7 @@ type Client interface {
|
||||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||||
DeleteTunnel(tunnelID uuid.UUID) error
|
DeleteTunnel(tunnelID uuid.UUID) error
|
||||||
ListTunnels(filter *Filter) ([]*Tunnel, error)
|
ListTunnels(filter *Filter) ([]*Tunnel, error)
|
||||||
|
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
||||||
CleanupConnections(tunnelID uuid.UUID) error
|
CleanupConnections(tunnelID uuid.UUID) error
|
||||||
RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error)
|
RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error)
|
||||||
|
|
||||||
|
@ -336,6 +348,28 @@ func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
|
||||||
return tunnels, err
|
return tunnels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
|
||||||
|
endpoint := r.baseEndpoints.accountLevel
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||||
|
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return parseConnectionsDetails(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, r.statusCodeToError("list connection details", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConnectionsDetails(reader io.Reader) ([]*ActiveClient, error) {
|
||||||
|
var clients []*ActiveClient
|
||||||
|
err := parseResponse(reader, &clients)
|
||||||
|
return clients, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID) error {
|
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID) error {
|
||||||
endpoint := r.baseEndpoints.accountLevel
|
endpoint := r.baseEndpoints.accountLevel
|
||||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -205,3 +206,24 @@ func TestUnmarshalTunnelErr(t *testing.T) {
|
||||||
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
|
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalConnections(t *testing.T) {
|
||||||
|
jsonBody := `{"success":true,"messages":[],"errors":[],"result":[{"id":"d4041254-91e3-4deb-bd94-b46e11680b1e","features":["ha-origin"],"version":"2021.2.5","arch":"darwin_amd64","conns":[{"colo_name":"LIS","id":"ac2286e5-c708-4588-a6a0-ba6b51940019","is_pending_reconnect":false,"origin_ip":"148.38.28.2","opened_at":"0001-01-01T00:00:00Z"}],"run_at":"0001-01-01T00:00:00Z"}]}`
|
||||||
|
expected := ActiveClient{
|
||||||
|
ID: uuid.MustParse("d4041254-91e3-4deb-bd94-b46e11680b1e"),
|
||||||
|
Features: []string{"ha-origin"},
|
||||||
|
Version: "2021.2.5",
|
||||||
|
Arch: "darwin_amd64",
|
||||||
|
RunAt: time.Time{},
|
||||||
|
Connections: []Connection{{
|
||||||
|
ID: uuid.MustParse("ac2286e5-c708-4588-a6a0-ba6b51940019"),
|
||||||
|
ColoName: "LIS",
|
||||||
|
IsPendingReconnect: false,
|
||||||
|
OriginIP: net.ParseIP("148.38.28.2"),
|
||||||
|
OpenedAt: time.Time{},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []*ActiveClient{&expected}, actual)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue