github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/internal/commands/tunnel_up.go (about)

     1  package commands
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"log"
    11  	"net"
    12  	"os"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"regexp"
    16  	"strconv"
    17  	"strings"
    18  	"text/template"
    19  
    20  	"github.com/Masterminds/semver"
    21  	"github.com/aws/aws-sdk-go/aws"
    22  	"github.com/aws/aws-sdk-go/aws/session"
    23  	"github.com/aws/aws-sdk-go/service/ec2instanceconnect"
    24  	"github.com/aws/aws-sdk-go/service/ssm"
    25  	"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
    26  	"github.com/hazelops/ize/internal/config"
    27  	"github.com/hazelops/ize/internal/requirements"
    28  	"github.com/hazelops/ize/pkg/term"
    29  	"github.com/pterm/pterm"
    30  	"github.com/sirupsen/logrus"
    31  	"github.com/spf13/cobra"
    32  	"golang.org/x/crypto/ssh"
    33  	"golang.org/x/crypto/ssh/terminal"
    34  )
    35  
    36  const sshConfig = `# SSH over Session Manager
    37  host i-* mi-*
    38  ServerAliveInterval 180
    39  ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
    40  
    41  {{range $k :=  .}}LocalForward {{$k}}
    42  {{end}}
    43  `
    44  
    45  var explainTunnelUpTmpl = `
    46  # Set variables
    47  SSH_CONFIG={{.EnvDir}}/ssh.config
    48  SSH_PUBLIC_KEY=$(cat ~/.ssh/id_rsa.pub)
    49  
    50  # Get bastion instance id
    51  BASTION_INSTANCE_ID=$(aws ssm get-parameter --name "/{{.Env}}/terraform-output" --with-decryption | jq -r '.Parameter.Value' | base64 -d | jq -r '.bastion_instance_id.value'
    52  
    53  # Get ssh config
    54  aws ssm get-parameter --name "/{{.Env}}/terraform-output" --with-decryption | jq -r '.Parameter.Value' | base64 -d | jq -r '.ssh_forward_config.value[]' > $SSH_CONFIG
    55  
    56  # Send ssh public key to instance
    57  aws ssm send-command --instance-ids $BASTION_INSTANCE_ID --document-name AWS-RunShellScript --comment 'Add an SSH public key to authorized_keys' --parameters '{"commands": ["grep -qR \"$(SSH_PUBLIC_KEY)\" /home/ubuntu/.ssh/authorized_keys || echo \"$(SSH_PUBLIC_KEY)\" >> /home/ubuntu/.ssh/authorized_keys"]}' 1> /dev/null)
    58  
    59  # Change to the dir and up tunnel
    60  (cd {{.EnvDir}} && $(aws ssm get-parameter --name "/{{.Env}}/terraform-output" --with-decryption | jq -r '.Parameter.Value' | base64 -d | jq -r '.cmd.value.tunnel.up') -F $SSH_CONFIG)
    61  `
    62  
    63  type TunnelUpOptions struct {
    64  	Config                *config.Project
    65  	PrivateKeyFile        string
    66  	PublicKeyFile         string
    67  	BastionHostID         string
    68  	ForwardHost           []string
    69  	StrictHostKeyChecking bool
    70  	Metadata              bool
    71  	Explain               bool
    72  }
    73  
    74  func NewTunnelUpFlags(project *config.Project) *TunnelUpOptions {
    75  	return &TunnelUpOptions{
    76  		Config: project,
    77  	}
    78  }
    79  
    80  func NewCmdTunnelUp(project *config.Project) *cobra.Command {
    81  	o := NewTunnelUpFlags(project)
    82  
    83  	cmd := &cobra.Command{
    84  		Use:   "up",
    85  		Short: "Open tunnel with sending ssh key",
    86  		Long:  "Open tunnel with sending ssh key to remote server",
    87  		RunE: func(cmd *cobra.Command, args []string) error {
    88  			cmd.SilenceUsage = true
    89  
    90  			if o.Explain {
    91  				err := o.Config.Generate(explainTunnelUpTmpl, nil)
    92  				if err != nil {
    93  					return err
    94  				}
    95  
    96  				return nil
    97  			}
    98  
    99  			err := o.Complete()
   100  			if err != nil {
   101  				return err
   102  			}
   103  
   104  			err = o.Validate()
   105  			if err != nil {
   106  				return err
   107  			}
   108  
   109  			err = o.Run()
   110  			if err != nil {
   111  				return err
   112  			}
   113  
   114  			return nil
   115  		},
   116  	}
   117  
   118  	cmd.Flags().StringVar(&o.BastionHostID, "bastion-instance-id", "", "set bastion host instance id (i-xxxxxxxxxxxxxxxxx)")
   119  	cmd.Flags().StringSliceVar(&o.ForwardHost, "forward-host", nil, "set forward hosts for redirect with next format: <remote-host>:<remote-port>, <remote-host>:<remote-port>, <remote-host>:<remote-port>. In this case a free local port will be selected automatically.  It's possible to set local manually using <remote-host>:<remote-port>:<local-port>")
   120  	cmd.Flags().StringVar(&o.PublicKeyFile, "ssh-public-key", "", "set ssh key public path")
   121  	cmd.Flags().StringVar(&o.PrivateKeyFile, "ssh-private-key", "", "set ssh key private path")
   122  	cmd.PersistentFlags().BoolVar(&o.StrictHostKeyChecking, "strict-host-key-checking", false, "set strict host key checking")
   123  	cmd.PersistentFlags().BoolVar(&o.Metadata, "use-ec2-metadata", false, "send ssh key to EC2 metadata (work only for Ubuntu versions > 20.0)")
   124  	cmd.Flags().BoolVar(&o.Explain, "explain", false, "bash alternative shown")
   125  
   126  	return cmd
   127  }
   128  
   129  func (o *TunnelUpOptions) Complete() error {
   130  	if err := requirements.CheckRequirements(requirements.WithSSMPlugin()); err != nil {
   131  		return err
   132  	}
   133  
   134  	isUp, err := checkTunnel(o.Config.EnvDir)
   135  	if err != nil {
   136  		return fmt.Errorf("can't run tunnel up: %w", err)
   137  	}
   138  	if isUp {
   139  		os.Exit(0)
   140  	}
   141  
   142  	if o.PrivateKeyFile == "" && o.Config.Tunnel != nil {
   143  		o.PrivateKeyFile = o.Config.Tunnel.SSHPrivateKey
   144  	}
   145  
   146  	if o.PublicKeyFile == "" && o.Config.Tunnel != nil {
   147  		o.PublicKeyFile = o.Config.Tunnel.SSHPublicKey
   148  	}
   149  
   150  	if o.PrivateKeyFile == "" {
   151  		home, _ := os.UserHomeDir()
   152  		o.PrivateKeyFile = fmt.Sprintf("%s/.ssh/id_rsa", home)
   153  	}
   154  
   155  	if o.PublicKeyFile == "" {
   156  		home, _ := os.UserHomeDir()
   157  		o.PublicKeyFile = fmt.Sprintf("%s/.ssh/id_rsa.pub", home)
   158  	}
   159  
   160  	if len(o.BastionHostID) == 0 && len(o.ForwardHost) != 0 {
   161  		return fmt.Errorf("can't load options for a command: --forward-host parameter requires --bastion-instance-id")
   162  	}
   163  
   164  	if len(o.ForwardHost) == 0 && len(o.BastionHostID) != 0 {
   165  		return fmt.Errorf("can't load options for a command: --bastion-instance-id requires --forward-host parameter")
   166  	}
   167  
   168  	if len(o.BastionHostID) == 0 && len(o.ForwardHost) == 0 {
   169  		if o.Config.Tunnel != nil {
   170  			o.ForwardHost = o.Config.Tunnel.ForwardHost
   171  			o.BastionHostID = o.Config.Tunnel.BastionInstanceID
   172  		}
   173  	}
   174  
   175  	if len(o.BastionHostID) == 0 && len(o.ForwardHost) == 0 {
   176  		wr := new(SSMWrapper)
   177  		wr.Api = ssm.New(o.Config.Session)
   178  		bastionHostID, forwardHost, err := writeSSHConfigFromSSM(wr, o.Config.Env, o.Config.EnvDir)
   179  		if err != nil {
   180  			return err
   181  		}
   182  
   183  		o.BastionHostID = bastionHostID
   184  		o.ForwardHost = forwardHost
   185  		pterm.Success.Println("Tunnel forwarding configuration obtained from SSM")
   186  	} else {
   187  		err := writeSSHConfigFromConfig(o.ForwardHost, o.Config.EnvDir)
   188  		if err != nil {
   189  			return err
   190  		}
   191  		pterm.Success.Println("Tunnel forwarding configuration obtained from the config file")
   192  	}
   193  
   194  	return nil
   195  }
   196  
   197  func (o *TunnelUpOptions) Validate() error {
   198  	if len(o.Config.Env) == 0 {
   199  		return fmt.Errorf("env must be specified")
   200  	}
   201  
   202  	for _, h := range o.ForwardHost {
   203  		p, _ := strconv.Atoi(strings.Split(h, ":")[2])
   204  		if err := checkPort(p, o.Config.EnvDir); err != nil {
   205  			return fmt.Errorf("tunnel forwarding config validation failed: %w", err)
   206  		}
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  func (o *TunnelUpOptions) Run() error {
   213  	logrus.Debugf("public key path: %s", o.PublicKeyFile)
   214  	logrus.Debugf("private key path: %s", o.PrivateKeyFile)
   215  
   216  	err := o.checkOsVersion()
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	pk, err := getPublicKey(o.PublicKeyFile)
   222  	if err != nil {
   223  		return fmt.Errorf("can't get public key: %s", err)
   224  	}
   225  
   226  	logrus.Debugf("public key:\n%s", pk)
   227  
   228  	if o.Metadata {
   229  		err = sendSSHPublicKey(o.BastionHostID, pk, o.Config.Session)
   230  		if err != nil {
   231  			return fmt.Errorf("can't run tunnel: %s", err)
   232  		}
   233  	} else {
   234  		err = sendSSHPublicKeyLegacy(o.BastionHostID, pk, o.Config.Session)
   235  		if err != nil {
   236  			return fmt.Errorf("can't run tunnel: %s", err)
   237  		}
   238  	}
   239  
   240  	forwardConfig, err := o.upTunnel()
   241  	if err != nil {
   242  		return err
   243  	}
   244  
   245  	pterm.Success.Println("Tunnel is up! Forwarded ports:")
   246  	pterm.Println(forwardConfig)
   247  
   248  	return nil
   249  }
   250  
   251  func (o *TunnelUpOptions) checkOsVersion() error {
   252  	diio, err := o.Config.AWSClient.SSMClient.DescribeInstanceInformation(&ssm.DescribeInstanceInformationInput{
   253  		Filters: []*ssm.InstanceInformationStringFilter{
   254  			{
   255  				Key:    aws.String("InstanceIds"),
   256  				Values: aws.StringSlice([]string{o.BastionHostID}),
   257  			},
   258  		},
   259  	})
   260  	if err != nil {
   261  		return fmt.Errorf("can't get instance '%s' information: %s", o.BastionHostID, err)
   262  	}
   263  
   264  	if len(diio.InstanceInformationList) == 0 {
   265  		return fmt.Errorf("can't get instance '%s' information", o.BastionHostID)
   266  	}
   267  
   268  	osName := *diio.InstanceInformationList[0].PlatformName
   269  	osVersion := *diio.InstanceInformationList[0].PlatformVersion
   270  
   271  	switch osName {
   272  	case "Ubuntu":
   273  		if semver.MustParse(osVersion).LessThan(semver.MustParse("20.04")) {
   274  			pterm.Warning.Printfln("Your bastion host AMI is Ubuntu %s, Instance Connect is not installed by default on that version of OS. Please upgrade to at least v20.04", osVersion)
   275  		}
   276  	case "Amazon Linux AMI":
   277  		if semver.MustParse(osVersion).LessThan(semver.MustParse("2.0.20190618")) {
   278  			pterm.Warning.Printfln("Your bastion host AMI is Amazon Linux AMI %s, Instance Connect is not installed by default on that version of OS. Please upgrade your AMI to at least 2.0.20190618", osVersion)
   279  		}
   280  	}
   281  
   282  	return nil
   283  }
   284  
   285  func (o *TunnelUpOptions) upTunnel() (string, error) {
   286  	sshConfigPath := fmt.Sprintf("%s/ssh.config", o.Config.EnvDir)
   287  	logrus.Debugf("ssh config path: %s", sshConfigPath)
   288  
   289  	if err := setAWSCredentials(o.Config.Session); err != nil {
   290  		return "", fmt.Errorf("can't run tunnel: %w", err)
   291  	}
   292  
   293  	args := o.getSSHCommandArgs(sshConfigPath)
   294  
   295  	err := o.runSSH(args)
   296  	if err != nil {
   297  		return "", err
   298  	}
   299  
   300  	var forwardConfig string
   301  	for _, h := range o.ForwardHost {
   302  		ss := strings.Split(h, ":")
   303  		forwardConfig += fmt.Sprintf("%s:%s ➡ localhost:%s\n", ss[0], ss[1], ss[2])
   304  	}
   305  	return forwardConfig, nil
   306  }
   307  
   308  func (o *TunnelUpOptions) runSSH(args []string) error {
   309  	c := exec.Command("ssh", args...)
   310  
   311  	c.Dir = o.Config.EnvDir
   312  	os.Setenv("AWS_REGION", o.Config.AwsRegion)
   313  
   314  	runner := term.New(term.WithStdin(os.Stdin))
   315  	_, _, code, err := runner.Run(c)
   316  	if err != nil {
   317  		return err
   318  	}
   319  
   320  	if code != 0 {
   321  		return fmt.Errorf("exit status: %d", code)
   322  	}
   323  	return nil
   324  }
   325  
   326  func (o *TunnelUpOptions) getSSHCommandArgs(sshConfigPath string) []string {
   327  	args := []string{"-M", "-t", "-S", "bastion.sock", "-fN"}
   328  	if !o.StrictHostKeyChecking {
   329  		args = append(args, "-o", "StrictHostKeyChecking=no")
   330  	}
   331  	args = append(args, fmt.Sprintf("ubuntu@%s", o.BastionHostID))
   332  	args = append(args, "-F", sshConfigPath)
   333  
   334  	if _, err := os.Stat(o.PrivateKeyFile); !os.IsNotExist(err) {
   335  		args = append(args, "-i", o.PrivateKeyFile)
   336  	}
   337  
   338  	if o.Config.LogLevel == "debug" {
   339  		args = append(args, "-vvv")
   340  	}
   341  
   342  	return args
   343  }
   344  
   345  type SSMWrapper struct {
   346  	Api ssmiface.SSMAPI
   347  }
   348  
   349  func getTerraformOutput(wr *SSMWrapper, env string) (terraformOutput, error) {
   350  	resp, err := wr.Api.GetParameter(&ssm.GetParameterInput{
   351  		Name:           aws.String(fmt.Sprintf("/%s/terraform-output", env)),
   352  		WithDecryption: aws.Bool(true),
   353  	})
   354  	if err != nil {
   355  		return terraformOutput{}, fmt.Errorf("can't get terraform output: %w", err)
   356  	}
   357  
   358  	var value []byte
   359  
   360  	value, err = base64.StdEncoding.DecodeString(*resp.Parameter.Value)
   361  	if err != nil {
   362  		return terraformOutput{}, fmt.Errorf("can't get terraform output: %w", err)
   363  	}
   364  
   365  	logrus.Debugf("decoded terrafrom output: \n%s", value)
   366  
   367  	var output terraformOutput
   368  
   369  	err = json.Unmarshal(value, &output)
   370  	if err != nil {
   371  		return terraformOutput{}, fmt.Errorf("can't get terraform output: %w", err)
   372  	}
   373  
   374  	logrus.Debugf("output: %s", output)
   375  
   376  	return output, nil
   377  }
   378  
   379  type terraformOutput struct {
   380  	BastionInstanceID struct {
   381  		Value string `json:"value,omitempty"`
   382  	} `json:"bastion_instance_id,omitempty"`
   383  	SSHForwardConfig struct {
   384  		Value []string `json:"value,omitempty"`
   385  	} `json:"ssh_forward_config,omitempty"`
   386  }
   387  
   388  func sendSSHPublicKey(bastionID string, key string, sess *session.Session) error {
   389  	_, err := ec2instanceconnect.New(sess).SendSSHPublicKey(&ec2instanceconnect.SendSSHPublicKeyInput{
   390  		InstanceId:     aws.String(bastionID),
   391  		InstanceOSUser: aws.String("ubuntu"),
   392  		SSHPublicKey:   aws.String(key),
   393  	})
   394  	if err != nil {
   395  		return err
   396  	}
   397  
   398  	return nil
   399  }
   400  
   401  func sendSSHPublicKeyLegacy(bastionID string, key string, sess *session.Session) error {
   402  	// This command is executed in the bastion host and it checks if our public key is present. If it's not it uploads it to _authorized_keys file.
   403  	command := fmt.Sprintf(
   404  		`grep -qR "%s" /home/ubuntu/.ssh/authorized_keys || echo "%s" >> /home/ubuntu/.ssh/authorized_keys`,
   405  		strings.TrimSpace(key), strings.TrimSpace(key),
   406  	)
   407  
   408  	logrus.Debugf("send command: \n%s", command)
   409  
   410  	_, err := ssm.New(sess).SendCommand(&ssm.SendCommandInput{
   411  		InstanceIds:  []*string{&bastionID},
   412  		DocumentName: aws.String("AWS-RunShellScript"),
   413  		Comment:      aws.String("Add an SSH public key to authorized_keys"),
   414  		Parameters: map[string][]*string{
   415  			"commands": {&command},
   416  		},
   417  	})
   418  	if err != nil {
   419  		return fmt.Errorf("can't send SSH public key: %w", err)
   420  	}
   421  
   422  	return nil
   423  }
   424  
   425  func getPublicKey(path string) (string, error) {
   426  	if !filepath.IsAbs(path) {
   427  		var err error
   428  		path, err = filepath.Abs(path)
   429  		if err != nil {
   430  			return "", err
   431  		}
   432  	}
   433  
   434  	if _, err := os.Stat(path); err != nil {
   435  		return "", fmt.Errorf("%s does not exist", path)
   436  	}
   437  
   438  	f, err := ioutil.ReadFile(path)
   439  	if err != nil {
   440  		return "", err
   441  	}
   442  
   443  	_, _, _, _, err = ssh.ParseAuthorizedKey(f)
   444  	if err != nil {
   445  		return "", err
   446  	}
   447  
   448  	return string(f), nil
   449  }
   450  
   451  func getHosts(config string) [][]string {
   452  	// This regexp reads ssh.conf configuration, so we can display it nicely in the UI
   453  	re, err := regexp.Compile(`LocalForward\s(?P<localPort>\d+)\s(?P<remoteHost>.+):(?P<remotePort>\d+)`)
   454  	if err != nil {
   455  		log.Fatal(fmt.Errorf("can't get forward config: %w", err))
   456  	}
   457  
   458  	hosts := re.FindAllStringSubmatch(
   459  		config,
   460  		-1,
   461  	)
   462  
   463  	return hosts
   464  }
   465  
   466  func getSSHConfig(path string) (string, error) {
   467  	f, err := os.Open(path)
   468  	if err != nil {
   469  		return "", fmt.Errorf("can't get ssh config: %w", err)
   470  	}
   471  
   472  	b, err := io.ReadAll(f)
   473  	if err != nil {
   474  		return "", fmt.Errorf("can't get ssh config: %w", err)
   475  	}
   476  
   477  	return string(b), nil
   478  }
   479  
   480  func getFreePort() (int, error) {
   481  	addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
   482  	if err != nil {
   483  		return 0, err
   484  	}
   485  
   486  	l, err := net.ListenTCP("tcp", addr)
   487  	if err != nil {
   488  		return 0, err
   489  	}
   490  	defer func(l *net.TCPListener) {
   491  		err := l.Close()
   492  		if err != nil {
   493  			log.Fatal(err)
   494  		}
   495  	}(l)
   496  	return l.Addr().(*net.TCPAddr).Port, nil
   497  }
   498  
   499  func checkPort(port int, dir string) error {
   500  	addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("127.0.0.1:%d", port))
   501  	if err != nil {
   502  		return fmt.Errorf("can't check address %s: %w", fmt.Sprintf("127.0.0.1:%d", port), err)
   503  	}
   504  
   505  	l, err := net.ListenTCP("tcp", addr)
   506  	if err != nil {
   507  		command := fmt.Sprintf("lsof -i tcp:%d | grep LISTEN | awk '{print $1, $2}'", port)
   508  		stdout, stderr, code, err := term.New(term.WithStdout(io.Discard), term.WithStderr(io.Discard)).Run(exec.Command("bash", "-c", command))
   509  		if err != nil {
   510  			return fmt.Errorf("can't run command '%s': %w", command, err)
   511  		}
   512  		if code == 0 {
   513  			stdout = strings.TrimSpace(stdout)
   514  			processName := strings.Split(stdout, " ")[0]
   515  			processPid, err := strconv.Atoi(strings.Split(stdout, " ")[1])
   516  			if err != nil {
   517  				return fmt.Errorf("can't get pid: %w", err)
   518  			}
   519  			pterm.Info.Printfln("Can't start tunnel on port %d. It seems like it's take by a process '%s'.", port, processName)
   520  			proc, err := os.FindProcess(processPid)
   521  			if err != nil {
   522  				return fmt.Errorf("can't find process: %w", err)
   523  			}
   524  
   525  			_, err = os.Stat(filepath.Join(dir, "bastion.sock"))
   526  			if processName == "ssh" && os.IsNotExist(err) {
   527  				return fmt.Errorf("it could be another ize tunnel, but we can't find a socket. Something went wrong. We suggest terminating it and starting it up again")
   528  			}
   529  			isContinue := false
   530  			if terminal.IsTerminal(int(os.Stdout.Fd())) {
   531  				isContinue, err = pterm.DefaultInteractiveConfirm.WithDefaultText("Would you like to terminate it?").Show()
   532  				if err != nil {
   533  					return err
   534  				}
   535  			} else {
   536  				isContinue = true
   537  			}
   538  
   539  			if !isContinue {
   540  				return fmt.Errorf("destroying was canceled")
   541  			}
   542  			err = proc.Kill()
   543  			if err != nil {
   544  				return fmt.Errorf("can't kill process: %w", err)
   545  			}
   546  
   547  			pterm.Info.Printfln("Process '%s' (pid %d) was killed", processName, processPid)
   548  
   549  			return nil
   550  		}
   551  		return fmt.Errorf("error during run command: %s (exit code: %d, stderr: %s)", command, code, stderr)
   552  	}
   553  
   554  	err = l.Close()
   555  	if err != nil {
   556  		return err
   557  	}
   558  
   559  	return nil
   560  }
   561  
   562  func writeSSHConfigFromSSM(wr *SSMWrapper, env string, dir string) (string, []string, error) {
   563  	var bastionHostID string
   564  	var forwardHost []string
   565  
   566  	to, err := getTerraformOutput(wr, env)
   567  	if err != nil {
   568  		return "", []string{}, fmt.Errorf("can't write SSH config: %w", err)
   569  	}
   570  
   571  	sshConfigPath := fmt.Sprintf("%s/ssh.config", dir)
   572  
   573  	f, err := os.Create(sshConfigPath)
   574  	if err != nil {
   575  		return "", []string{}, fmt.Errorf("can't write SSH config: %w", err)
   576  	}
   577  
   578  	sshConfig := strings.Join(to.SSHForwardConfig.Value, "\n")
   579  	_, err = io.WriteString(f, sshConfig)
   580  	if err != nil {
   581  		return "", []string{}, fmt.Errorf("can't write SSH config: %w", err)
   582  	}
   583  	if err = f.Close(); err != nil {
   584  		return "", []string{}, fmt.Errorf("can't write SSH config: %w", err)
   585  	}
   586  
   587  	hosts := getHosts(sshConfig)
   588  	if len(hosts) == 0 {
   589  		errMsg := "can't write SSH config: forwarding config is not valid"
   590  		if logrus.GetLevel() == logrus.DebugLevel {
   591  			errMsg += fmt.Sprintf(". Config in SSM: \n%s", sshConfig)
   592  		}
   593  		return "", []string{}, fmt.Errorf(errMsg)
   594  	}
   595  
   596  	bastionHostID = to.BastionInstanceID.Value
   597  
   598  	for _, h := range hosts {
   599  		forwardHost = append(forwardHost, fmt.Sprintf("%s:%s:%s", h[2], h[3], h[1]))
   600  	}
   601  
   602  	return bastionHostID, forwardHost, nil
   603  }
   604  
   605  func writeSSHConfigFromConfig(forwardHost []string, dir string) error {
   606  	sshConfigPath := fmt.Sprintf("%s/ssh.config", dir)
   607  	f, err := os.Create(sshConfigPath)
   608  	if err != nil {
   609  		return fmt.Errorf("can't run tunnel up: %w", err)
   610  	}
   611  
   612  	var tmplData []string
   613  	for k, v := range forwardHost {
   614  		ss := strings.Split(v, ":")
   615  		if len(ss) < 2 || len(ss) > 3 {
   616  			return fmt.Errorf("can't load options for a command: invalid format for forward host (should be host:port:localport)")
   617  		}
   618  		if len(ss) == 2 {
   619  			p, err := getFreePort()
   620  			if err != nil {
   621  				return fmt.Errorf("can't load options for a command: %w", err)
   622  			}
   623  			forwardHost[k] = forwardHost[k] + ":" + strconv.Itoa(p)
   624  			ss = append(ss, strconv.Itoa(p))
   625  		} else if len(ss[2]) == 0 {
   626  			return fmt.Errorf("can't load options for a command: invalid format for forward host (should be host:port:localport)")
   627  		}
   628  		tmplData = append(tmplData, fmt.Sprintf("%s %s:%s", ss[2], ss[0], ss[1]))
   629  	}
   630  	t := template.New("sshConfig")
   631  	t, err = t.Parse(sshConfig)
   632  	if err != nil {
   633  		return err
   634  	}
   635  	err = t.Execute(f, tmplData)
   636  	if err != nil {
   637  		return err
   638  	}
   639  	if err = f.Close(); err != nil {
   640  		return fmt.Errorf("can't run tunnel up: %w", err)
   641  	}
   642  
   643  	return nil
   644  }
   645  
   646  func checkTunnel(dir string) (bool, error) {
   647  	pathToSocket := filepath.Join(dir, "bastion.sock")
   648  	if _, err := os.Stat(pathToSocket); !os.IsNotExist(err) {
   649  		pterm.Info.Printfln("A socket file from another tunnel has been detected: %s", pathToSocket)
   650  		c := exec.Command(
   651  			"ssh", "-S", "bastion.sock", "-O", "check", "",
   652  		)
   653  		out := &bytes.Buffer{}
   654  		c.Stdout = out
   655  		c.Stderr = out
   656  		c.Dir = dir
   657  
   658  		err := c.Run()
   659  		if err == nil {
   660  			sshConfigPath := fmt.Sprintf("%s/ssh.config", dir)
   661  			sshConfig, err := getSSHConfig(sshConfigPath)
   662  			if err != nil {
   663  				return false, fmt.Errorf("can't check tunnel: %w", err)
   664  			}
   665  
   666  			pterm.Success.Println("Tunnel is up. Forwarding config:")
   667  			hosts := getHosts(sshConfig)
   668  			var forwardConfig string
   669  			for _, h := range hosts {
   670  				forwardConfig += fmt.Sprintf("%s:%s ➡ localhost:%s\n", h[2], h[3], h[1])
   671  			}
   672  			pterm.Println(forwardConfig)
   673  
   674  			return true, nil
   675  		} else {
   676  			pterm.Warning.Println("Tunnel socket file seems to be not useable. We have deleted it")
   677  			err := os.Remove(pathToSocket)
   678  			if err != nil {
   679  				return false, err
   680  			}
   681  			return false, nil
   682  		}
   683  	}
   684  
   685  	return false, nil
   686  }
   687  
   688  func setAWSCredentials(sess *session.Session) error {
   689  	v, err := sess.Config.Credentials.Get()
   690  	if err != nil {
   691  		return fmt.Errorf("can't set AWS credentials: %w", err)
   692  	}
   693  
   694  	err = os.Setenv("AWS_SECRET_ACCESS_KEY", v.SecretAccessKey)
   695  	if err != nil {
   696  		return err
   697  	}
   698  	err = os.Setenv("AWS_ACCESS_KEY_ID", v.AccessKeyID)
   699  	if err != nil {
   700  		return err
   701  	}
   702  	err = os.Setenv("AWS_SESSION_TOKEN", v.SessionToken)
   703  	if err != nil {
   704  		return err
   705  	}
   706  
   707  	return nil
   708  }