github.com/swisscom/cloudfoundry-cli@v7.1.0+incompatible/cf/commands/application/ssh.go (about)

     1  package application
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"time"
     8  
     9  	"golang.org/x/crypto/ssh"
    10  
    11  	"code.cloudfoundry.org/cli/cf/commandregistry"
    12  	"code.cloudfoundry.org/cli/cf/commands"
    13  	"code.cloudfoundry.org/cli/cf/configuration/coreconfig"
    14  	"code.cloudfoundry.org/cli/cf/flags"
    15  	. "code.cloudfoundry.org/cli/cf/i18n"
    16  	"code.cloudfoundry.org/cli/cf/net"
    17  	"code.cloudfoundry.org/cli/cf/requirements"
    18  	sshCmd "code.cloudfoundry.org/cli/cf/ssh"
    19  	"code.cloudfoundry.org/cli/cf/ssh/options"
    20  	sshTerminal "code.cloudfoundry.org/cli/cf/ssh/terminal"
    21  	"code.cloudfoundry.org/cli/cf/terminal"
    22  )
    23  
    24  type SSH struct {
    25  	ui            terminal.UI
    26  	config        coreconfig.Reader
    27  	gateway       net.Gateway
    28  	appReq        requirements.ApplicationRequirement
    29  	sshCodeGetter commands.SSHCodeGetter
    30  	opts          *options.SSHOptions
    31  	secureShell   sshCmd.SecureShell
    32  }
    33  
    34  type sshInfo struct {
    35  	SSHEndpoint            string `json:"app_ssh_endpoint"`
    36  	SSHEndpointFingerprint string `json:"app_ssh_host_key_fingerprint"`
    37  }
    38  
    39  func init() {
    40  	commandregistry.Register(&SSH{})
    41  }
    42  
    43  func (cmd *SSH) MetaData() commandregistry.CommandMetadata {
    44  	fs := make(map[string]flags.FlagSet)
    45  	fs["L"] = &flags.StringSliceFlag{ShortName: "L", Usage: T("Local port forward specification. This flag can be defined more than once.")}
    46  	fs["command"] = &flags.StringSliceFlag{Name: "command", ShortName: "c", Usage: T("Command to run. This flag can be defined more than once.")}
    47  	fs["app-instance-index"] = &flags.IntFlag{Name: "app-instance-index", ShortName: "i", Usage: T("Application instance index")}
    48  	fs["skip-host-validation"] = &flags.BoolFlag{Name: "skip-host-validation", ShortName: "k", Usage: T("Skip host key validation")}
    49  	fs["skip-remote-execution"] = &flags.BoolFlag{Name: "skip-remote-execution", ShortName: "N", Usage: T("Do not execute a remote command")}
    50  	fs["request-pseudo-tty"] = &flags.BoolFlag{Name: "request-pseudo-tty", ShortName: "t", Usage: T("Request pseudo-tty allocation")}
    51  	fs["force-pseudo-tty"] = &flags.BoolFlag{Name: "force-pseudo-tty", ShortName: "tt", Usage: T("Force pseudo-tty allocation")}
    52  	fs["disable-pseudo-tty"] = &flags.BoolFlag{Name: "disable-pseudo-tty", ShortName: "T", Usage: T("Disable pseudo-tty allocation")}
    53  
    54  	return commandregistry.CommandMetadata{
    55  		Name:        "ssh",
    56  		Description: T("SSH to an application container instance"),
    57  		Usage: []string{
    58  			T("CF_NAME ssh APP_NAME [-i app-instance-index] [-c command] [-L [bind_address:]port:host:hostport] [--skip-host-validation] [--skip-remote-execution] [--request-pseudo-tty] [--force-pseudo-tty] [--disable-pseudo-tty]"),
    59  		},
    60  		Flags: fs,
    61  	}
    62  }
    63  
    64  func (cmd *SSH) Requirements(requirementsFactory requirements.Factory, fc flags.FlagContext) ([]requirements.Requirement, error) {
    65  	if len(fc.Args()) != 1 {
    66  		cmd.ui.Failed(T("Incorrect Usage. Requires APP_NAME as argument") + "\n\n" + commandregistry.Commands.CommandUsage("ssh"))
    67  		return nil, fmt.Errorf("Incorrect usage: %d arguments of %d required", len(fc.Args()), 1)
    68  	}
    69  
    70  	var err error
    71  	cmd.opts, err = options.NewSSHOptions(fc)
    72  
    73  	if err != nil {
    74  		cmd.ui.Failed(fmt.Sprintf(T("Incorrect Usage:")+" %s\n\n%s", err.Error(), commandregistry.Commands.CommandUsage("ssh")))
    75  		return nil, err
    76  	}
    77  
    78  	cmd.appReq = requirementsFactory.NewApplicationRequirement(cmd.opts.AppName)
    79  
    80  	reqs := []requirements.Requirement{
    81  		requirementsFactory.NewLoginRequirement(),
    82  		requirementsFactory.NewTargetedSpaceRequirement(),
    83  		cmd.appReq,
    84  	}
    85  
    86  	return reqs, nil
    87  }
    88  
    89  func (cmd *SSH) SetDependency(deps commandregistry.Dependency, pluginCall bool) commandregistry.Command {
    90  	cmd.ui = deps.UI
    91  	cmd.config = deps.Config
    92  	cmd.gateway = deps.Gateways["cloud-controller"]
    93  
    94  	if deps.WildcardDependency != nil {
    95  		cmd.secureShell = deps.WildcardDependency.(sshCmd.SecureShell)
    96  	}
    97  
    98  	//get ssh-code for dependency
    99  	sshCodeGetter := commandregistry.Commands.FindCommand("ssh-code")
   100  	sshCodeGetter = sshCodeGetter.SetDependency(deps, false)
   101  	cmd.sshCodeGetter = sshCodeGetter.(commands.SSHCodeGetter)
   102  
   103  	return cmd
   104  }
   105  
   106  func (cmd *SSH) Execute(fc flags.FlagContext) error {
   107  	if fc.IsSet("i") {
   108  		instanceIndex := fc.Int("i")
   109  		if instanceIndex < 0 {
   110  			return fmt.Errorf(T("The application instance index cannot be negative"))
   111  		}
   112  		if instanceIndex >= cmd.appReq.GetApplication().InstanceCount {
   113  			return fmt.Errorf(T("The specified application instance does not exist"))
   114  		}
   115  	}
   116  
   117  	app := cmd.appReq.GetApplication()
   118  	info, err := cmd.getSSHEndpointInfo()
   119  	if err != nil {
   120  		return errors.New(T("Error getting SSH info:") + err.Error())
   121  	}
   122  
   123  	sshAuthCode, err := cmd.sshCodeGetter.Get()
   124  	if err != nil {
   125  		return errors.New(T("Error getting one time auth code: ") + err.Error())
   126  	}
   127  
   128  	//init secureShell if it is not already set by SetDependency() with fakes
   129  	if cmd.secureShell == nil {
   130  		cmd.secureShell = sshCmd.NewSecureShell(
   131  			sshCmd.DefaultSecureDialer(),
   132  			sshTerminal.DefaultHelper(),
   133  			sshCmd.DefaultListenerFactory(),
   134  			30*time.Second,
   135  			app,
   136  			info.SSHEndpointFingerprint,
   137  			info.SSHEndpoint,
   138  			sshAuthCode,
   139  		)
   140  	}
   141  
   142  	err = cmd.secureShell.Connect(cmd.opts)
   143  	if err != nil {
   144  		return errors.New(T("Error opening SSH connection: ") + err.Error())
   145  	}
   146  	defer cmd.secureShell.Close()
   147  
   148  	err = cmd.secureShell.LocalPortForward()
   149  	if err != nil {
   150  		return errors.New(T("Error forwarding port: ") + err.Error())
   151  	}
   152  
   153  	if cmd.opts.SkipRemoteExecution {
   154  		err = cmd.secureShell.Wait()
   155  	} else {
   156  		err = cmd.secureShell.InteractiveSession()
   157  	}
   158  
   159  	if err != nil {
   160  		if exitError, ok := err.(*ssh.ExitError); ok {
   161  			exitStatus := exitError.ExitStatus()
   162  			if sig := exitError.Signal(); sig != "" {
   163  				cmd.ui.Say(T("Process terminated by signal: {{.Signal}}. Exited with {{.ExitCode}}", map[string]interface{}{
   164  					"Signal":   sig,
   165  					"ExitCode": exitStatus,
   166  				}))
   167  			}
   168  			os.Exit(exitStatus)
   169  		} else {
   170  			return errors.New(T("Error: ") + err.Error())
   171  		}
   172  	}
   173  	return nil
   174  }
   175  
   176  func (cmd *SSH) getSSHEndpointInfo() (sshInfo, error) {
   177  	info := sshInfo{}
   178  	err := cmd.gateway.GetResource(cmd.config.APIEndpoint()+"/v2/info", &info)
   179  	return info, err
   180  }