diff --git a/clab/file.go b/clab/file.go index 9b208d4027..ee92d0e01b 100644 --- a/clab/file.go +++ b/clab/file.go @@ -35,6 +35,10 @@ type TopoFile struct { name string // file name without extension } +func (tf *TopoFile) GetDir() string { + return tf.dir +} + // GetTopology parses the topology file into c.Conf structure // as well as populates the TopoFile structure with the topology file related information func (c *CLab) GetTopology(topo, varsFile string) error { diff --git a/cmd/inspect.go b/cmd/inspect.go index aa3504944e..7c625ebafd 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -11,14 +11,17 @@ import ( "os" "path/filepath" "sort" + "strconv" "strings" "github.com/olekukonko/tablewriter" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/srl-labs/containerlab/clab" + "github.com/srl-labs/containerlab/nodes" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" + "github.com/srl-labs/containerlab/utils" + "github.com/srl-labs/containerlab/utils/mysocketio" ) var format string @@ -76,10 +79,6 @@ var inspectCmd = &cobra.Command{ return fmt.Errorf("failed to list containers: %s", err) } - if len(containers) == 0 { - log.Println("no containers found") - return nil - } if details { b, err := json.MarshalIndent(containers, "", " ") if err != nil { @@ -120,7 +119,6 @@ func printContainerInspect(c *clab.CLab, containers []types.GenericContainer, fo contDetails := make([]types.ContainerDetails, 0, len(containers)) // do not print published ports unless mysocketio kind is found printMysocket := false - var mysocketCID string for i := range containers { cont := &containers[i] @@ -145,7 +143,6 @@ func printContainerInspect(c *clab.CLab, containers []types.GenericContainer, fo cdet.Kind = kind if kind == "mysocketio" { printMysocket = true - mysocketCID = cont.ID } } if group, ok := cont.Labels["clab-node-group"]; ok { @@ -162,7 +159,22 @@ func printContainerInspect(c *clab.CLab, containers []types.GenericContainer, fo }) if format == "json" { - b, err := json.MarshalIndent(contDetails, "", " ") + resultJson := &types.JsonInspect{ContainerData: contDetails, MySocketIoData: []*types.MySocketIoEntry{}} + if printMysocket { + // search for the mysocketio token file by deducing it from the topology config binds section + tokenFile, err := deduceMySocketIoTokenFileFromBindMounts(c.Nodes, c.TopoFile.GetDir()) + if err != nil { + return err + } + // retrieve the MySocketIO Data + socketdata, err := getMySocketIoData(tokenFile) + if err != nil { + return fmt.Errorf("error when processing mysocketio data: %v", err) + } + resultJson.MySocketIoData = socketdata + } + + b, err := json.MarshalIndent(resultJson, "", " ") if err != nil { return fmt.Errorf("failed to marshal container details: %v", err) } @@ -197,19 +209,97 @@ func printContainerInspect(c *clab.CLab, containers []types.GenericContainer, fo return nil } - runtime := c.GlobalRuntime() - - stdout, stderr, err := runtime.Exec(context.Background(), mysocketCID, []string{"mysocketctl", "socket", "ls"}) + // search for the mysocketio token file by deducing it from the topology config binds section + tokenFile, err := deduceMySocketIoTokenFileFromBindMounts(c.Nodes, c.TopoFile.GetDir()) if err != nil { - return fmt.Errorf("failed to execute cmd: %v", err) - + return err } - if len(stderr) > 0 { - log.Infof("errors during listing mysocketio sockets: %s", string(stderr)) + // retrieve the MySocketIO Data + socketdata, err := getMySocketIoData(tokenFile) + if err != nil { + return fmt.Errorf("error when processing mysocketio data: %v", err) } + // prepare data for table + var tabDataMySocketIo [][]string + for _, entry := range socketdata { + var portstrarr []string + for _, port := range entry.Ports { + portstrarr = append(portstrarr, strconv.Itoa(port)) + } + tabDataMySocketIo = append(tabDataMySocketIo, []string{entry.SocketId, entry.DnsName, strings.Join(portstrarr, ", "), entry.Type, strconv.FormatBool(entry.CloudAuth), entry.Name}) + } + tableMySocketIo := tablewriter.NewWriter(os.Stdout) + headerMySocketIo := []string{ + "Socket ID", + "DNS Name", + "Ports", + "Type", + "Cloud Auth", + "Name", + } + // configure table output + tableMySocketIo.SetHeader(headerMySocketIo) + tableMySocketIo.SetAutoFormatHeaders(false) + tableMySocketIo.SetAutoWrapText(false) + tableMySocketIo.AppendBulk(tabDataMySocketIo) fmt.Println("Published ports:") - fmt.Println(string(stdout)) + tableMySocketIo.Render() return nil } + +// getMySocketioData uses the mysocketio.http client to retrieve the socket data +func getMySocketIoData(tokenfile string) ([]*types.MySocketIoEntry, error) { + result := []*types.MySocketIoEntry{} + + client, err := mysocketio.NewClient(tokenfile) + if err != nil { + return nil, err + } + + sockets := []mysocketio.Socket{} + err = client.Request("GET", "connect", &sockets, nil) + if err != nil { + return nil, err + } + + for _, s := range sockets { + newentry := &types.MySocketIoEntry{ + SocketId: s.SocketID, + DnsName: s.Dnsname, + Ports: s.SocketTcpPorts, + Type: s.SocketType, + CloudAuth: s.CloudAuthEnabled, + Name: s.Name, + } + result = append(result, newentry) + } + return result, nil +} + +// deduceMySocketIoTokenFileFromBindMounts searches through the topology to find a node of kind mysocketio. +// if that is found, the bindmounts are searched for ".mysocketio_token" and the path is being converted into an +// absolute path and returned. +// If the +func deduceMySocketIoTokenFileFromBindMounts(nodes map[string]nodes.Node, configPath string) (string, error) { + for _, node := range nodes { + // search for mysocketio kind in topology config + if node.Config().Kind == "mysocketio" { + // iterate through bind mounts + for _, bind := range node.Config().Binds { + // watch out for ".mysocketio_token" + if strings.Contains(bind, ".mysocketio_token") { + // split the bindmount and resolve the path to an absolute path + deduced_absfilepath := utils.ResolvePath(strings.Split(bind, ":")[0], configPath) + // check file existence before returning + if !utils.FileExists(deduced_absfilepath) { + return "", fmt.Errorf(".mysocketio_token resolved to %s, but that file doesn't exist", deduced_absfilepath) + } + return deduced_absfilepath, nil + } + } + } + } + return "", fmt.Errorf("unable to find \".mysocketio_token\"") +} diff --git a/types/types.go b/types/types.go index 5bebce2ac1..add23a929c 100644 --- a/types/types.go +++ b/types/types.go @@ -276,3 +276,17 @@ type ContainerDetails struct { IPv4Address string `json:"ipv4_address,omitempty"` IPv6Address string `json:"ipv6_address,omitempty"` } + +type MySocketIoEntry struct { + SocketId string `json:"socket_id,omitempty"` + DnsName string `json:"dns_name,omitempty"` + Ports []int `json:"ports,omitempty"` + Type string `json:"type,omitempty"` + CloudAuth bool `json:"cloud_auth,omitempty"` + Name string `json:"name,omitempty"` +} + +type JsonInspect struct { + ContainerData []ContainerDetails `json:"container_data"` + MySocketIoData []*MySocketIoEntry `json:"mysocketio_data"` +} diff --git a/utils/mysocketio/mysocketio.go b/utils/mysocketio/mysocketio.go new file mode 100644 index 0000000000..7a8392cb0d --- /dev/null +++ b/utils/mysocketio/mysocketio.go @@ -0,0 +1,111 @@ +package mysocketio + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + h "net/http" + "os" + "strings" +) + +type Client struct { + token string +} + +func GetApiUrl() string { + if os.Getenv("MYSOCKET_API") != "" { + return os.Getenv("MYSOCKET_API") + } else { + return "https://api.mysocket.io" + } +} + +func NewClient(tokenfile string) (*Client, error) { + token, err := GetToken(tokenfile) + if err != nil { + return nil, err + } + + c := &Client{token: token} + + return c, nil +} + +func (c *Client) Request(method string, url string, target interface{}, data interface{}) error { + jv, _ := json.Marshal(data) + body := bytes.NewBuffer(jv) + + req, _ := h.NewRequest(method, fmt.Sprintf("%s/%s", GetApiUrl(), url), body) + req.Header.Add("x-access-token", c.token) + req.Header.Set("Content-Type", "application/json") + client := &h.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + if resp.StatusCode == 401 { + return errors.New("no valid token, Please login") + } + + if resp.StatusCode < 200 || resp.StatusCode > 204 { + responseData, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("failed to create object (%d) %v", resp.StatusCode, string(responseData)) + } + + if resp.StatusCode == 204 { + return nil + } + + err = json.NewDecoder(resp.Body).Decode(target) + if err != nil { + return errors.New("failed to decode data") + } + + return nil +} + +func GetToken(tokenfile string) (string, error) { + if _, err := os.Stat(tokenfile); os.IsNotExist(err) { + return "", errors.New("please login first (no token found)") + } + content, err := ioutil.ReadFile(tokenfile) + if err != nil { + return "", err + } + + tokenString := strings.TrimRight(string(content), "\n") + return tokenString, nil +} + +type Socket struct { + Tunnels []Tunnel `json:"tunnels,omitempty"` + Username string `json:"user_name,omitempty"` + SocketID string `json:"socket_id,omitempty"` + SocketTcpPorts []int `json:"socket_tcp_ports,omitempty"` + Dnsname string `json:"dnsname,omitempty"` + Name string `json:"name,omitempty"` + SocketType string `json:"socket_type,omitempty"` + ProtectedSocket bool `json:"protected_socket"` + ProtectedUsername string `json:"protected_username"` + ProtectedPassword string `json:"protected_password"` + CloudAuthEnabled bool `json:"cloud_authentication_enabled,omitempty"` + AllowedEmailAddresses []string `json:"cloud_authentication_email_allowed_addressses,omitempty"` + AllowedEmailDomains []string `json:"cloud_authentication_email_allowed_domains,omitempty"` + SSHCa string `json:"ssh_ca,omitempty"` + UpstreamUsername string `json:"upstream_username,omitempty"` + UpstreamPassword string `json:"upstream_password,omitempty"` + UpstreamHttpHostname string `json:"upstream_http_hostname,omitempty"` + UpstreamType string `json:"upstream_type,omitempty"` +} + +type Tunnel struct { + TunnelID string `json:"tunnel_id,omitempty"` + LocalPort int `json:"local_port,omitempty"` + TunnelServer string `json:"tunnel_server,omitempty"` +}