package cmd import ( "archive/zip" "bytes" "crypto/tls" "crypto/x509" "encoding/json" "errors" "fmt" "io" "net/http" "os" "os/exec" "path" "strings" "github.com/rancher/cli/cliclient" managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3" "github.com/urfave/cli" ) const sshDescription = ` For any nodes created through Rancher using docker-machine, you can SSH into the node. This is not supported for any custom nodes. Examples: # SSH into a node by ID/name $ rancher ssh nodeFoo # SSH into a node by ID/name using the external IP address $ rancher ssh -e nodeFoo # SSH into a node by name but specify the login name to use $ rancher ssh -l login1 nodeFoo # SSH into a node by specifying login name and node using the @ syntax while adding a command to run $ rancher ssh login1@nodeFoo -- netstat -p tcp ` func SSHCommand() cli.Command { return cli.Command{ Name: "ssh", Usage: "SSH into a node", Description: sshDescription, Action: nodeSSH, ArgsUsage: "[NODE_ID/NODE_NAME]", Flags: []cli.Flag{ cli.BoolFlag{ Name: "external,e", Usage: "Use the external ip address of the node", }, cli.StringFlag{ Name: "login,l", Usage: "The login name", }, }, } } func nodeSSH(ctx *cli.Context) error { args := ctx.Args() if len(args) > 0 && (args[0] == "-h" || args[0] == "--help") { return cli.ShowCommandHelp(ctx, "ssh") } if ctx.NArg() == 0 { return cli.ShowCommandHelp(ctx, "ssh") } user := ctx.String("login") nodeName := ctx.Args().First() if strings.Contains(nodeName, "@") { user = strings.Split(nodeName, "@")[0] nodeName = strings.Split(nodeName, "@")[1] } args = args[1:] c, err := GetClient(ctx) if err != nil { return err } sshNode, key, err := getNodeAndKey(ctx, c, nodeName) if err != nil { return err } if user == "" { user = sshNode.SshUser } ipAddress := sshNode.IPAddress if ctx.Bool("external") { ipAddress = sshNode.ExternalIPAddress } return processExitCode(callSSH(key, ipAddress, user, args)) } func getNodeAndKey(ctx *cli.Context, c *cliclient.MasterClient, nodeName string) (managementClient.Node, []byte, error) { sshNode := managementClient.Node{} resource, err := Lookup(c, nodeName, "node") if err != nil { return sshNode, nil, err } sshNode, err = getNodeByID(ctx, c, resource.ID) if err != nil { return sshNode, nil, err } link := sshNode.Links["nodeConfig"] if link == "" { // Get the machine and use that instead. machine, err := getMachineByNodeName(ctx, c, sshNode.NodeName) if err != nil { return sshNode, nil, fmt.Errorf("failed to find SSH key for node [%s]", nodeName) } link = machine.Links["sshkeys"] } key, sshUser, err := getSSHKey(c, link, getNodeName(sshNode)) if err != nil { return sshNode, nil, err } if sshUser != "" { sshNode.SshUser = sshUser } return sshNode, key, nil } func callSSH(content []byte, ip string, user string, args []string) error { dest := fmt.Sprintf("%s@%s", user, ip) tmpfile, err := os.CreateTemp("", "ssh") if err != nil { return err } defer os.Remove(tmpfile.Name()) if err := os.Chmod(tmpfile.Name(), 0600); err != nil { return err } _, err = tmpfile.Write(content) if err != nil { return err } if err := tmpfile.Close(); err != nil { return err } cmd := exec.Command("ssh", append([]string{"-i", tmpfile.Name(), dest}, args...)...) cmd.Stdout = os.Stdout cmd.Stdin = os.Stdin cmd.Stderr = os.Stderr return cmd.Run() } func getSSHKey(c *cliclient.MasterClient, link, nodeName string) ([]byte, string, error) { if link == "" { return nil, "", fmt.Errorf("failed to find SSH key for %s", nodeName) } req, err := http.NewRequest("GET", link, nil) if err != nil { return nil, "", err } req.SetBasicAuth(c.UserConfig.AccessKey, c.UserConfig.SecretKey) req.Header.Add("Accept-Encoding", "zip") var tlsConfig *tls.Config if c.UserConfig.CACerts != "" { roots := x509.NewCertPool() ok := roots.AppendCertsFromPEM([]byte(c.UserConfig.CACerts)) if !ok { return []byte{}, "", err } tlsConfig = &tls.Config{ RootCAs: roots, } } client, err := newHTTPClient(c.UserConfig, tlsConfig) if err != nil { return nil, "", err } resp, err := client.Do(req) if err != nil { return nil, "", err } defer resp.Body.Close() zipFiles, err := io.ReadAll(resp.Body) if err != nil { return nil, "", err } if resp.StatusCode != 200 { return nil, "", fmt.Errorf("%s", zipFiles) } zipReader, err := zip.NewReader(bytes.NewReader(zipFiles), resp.ContentLength) if err != nil { return nil, "", err } var sshKey []byte var sshUser string for _, file := range zipReader.File { if path.Base(file.Name) == "id_rsa" { sshKey, err = readFile(file) if err != nil { return nil, "", err } } else if path.Base(file.Name) == "config.json" { config, err := readFile(file) if err != nil { return nil, "", err } var data map[string]interface{} err = json.Unmarshal(config, &data) if err != nil { return nil, "", err } sshUser, _ = data["SSHUser"].(string) } } if len(sshKey) == 0 { return sshKey, "", errors.New("can't find private key file") } return sshKey, sshUser, nil } func readFile(file *zip.File) ([]byte, error) { r, err := file.Open() if err != nil { return nil, err } defer r.Close() return io.ReadAll(r) }