github.com/jenkins-x/jx/v2@v2.1.155/pkg/cmd/shell.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"runtime"
     7  
     8  	"github.com/jenkins-x/jx/v2/pkg/cmd/helper"
     9  	survey "gopkg.in/AlecAivazis/survey.v1"
    10  
    11  	"github.com/spf13/cobra"
    12  
    13  	"github.com/jenkins-x/jx-logging/pkg/log"
    14  	"github.com/jenkins-x/jx/v2/pkg/cmd/opts"
    15  	"github.com/jenkins-x/jx/v2/pkg/cmd/templates"
    16  	"k8s.io/client-go/tools/clientcmd"
    17  
    18  	"io/ioutil"
    19  	"os"
    20  	"os/exec"
    21  	"sort"
    22  	"strings"
    23  
    24  	"github.com/jenkins-x/jx/v2/pkg/util"
    25  )
    26  
    27  const (
    28  	defaultRcFile = `
    29  if [ -f /etc/bashrc ]; then
    30      source /etc/bashrc
    31  fi
    32  if [ -f ~/.bashrc ]; then
    33      source ~/.bashrc
    34  fi
    35  if type -t dh_bash-completion >/dev/null; then
    36      if type -t __start_jx >/dev/null; then true; else
    37          source <(jx completion bash)
    38      fi
    39  fi
    40  `
    41  
    42  	zshRcFile = `
    43  if [ -f /etc/zshrc ]; then
    44      source /etc/zshrc
    45  fi
    46  if [ -f ~/.zshrc ]; then
    47      source ~/.zshrc
    48  fi
    49  `
    50  )
    51  
    52  type ShellOptions struct {
    53  	*opts.CommonOptions
    54  
    55  	Filter string
    56  }
    57  
    58  var (
    59  	shell_long = templates.LongDesc(`
    60  		Create a sub shell so that changes to the Kubernetes context, namespace or environment remain local to the shell.`)
    61  	shell_example = templates.Examples(`
    62  		# create a new shell where the context changes are local to the shell only
    63  		jx shell
    64  
    65  		# create a new shell using a specific named context
    66  		jx shell prod-cluster
    67  
    68  		# ends the current shell and returns to the previous Kubernetes context
    69  		exit
    70  `)
    71  )
    72  
    73  func NewCmdShell(commonOpts *opts.CommonOptions) *cobra.Command {
    74  	options := &ShellOptions{
    75  		CommonOptions: commonOpts,
    76  	}
    77  	cmd := &cobra.Command{
    78  		Use:     "shell",
    79  		Aliases: []string{"sh"},
    80  		Short:   "Create a sub shell so that changes to the Kubernetes context, namespace or environment remain local to the shell",
    81  		Long:    shell_long,
    82  		Example: shell_example,
    83  		Run: func(cmd *cobra.Command, args []string) {
    84  			options.Cmd = cmd
    85  			options.Args = args
    86  			err := options.Run()
    87  			helper.CheckErr(err)
    88  		},
    89  	}
    90  	cmd.Flags().StringVarP(&options.Filter, "filter", "f", "", "Filter the list of contexts to switch between using the given text")
    91  	return cmd
    92  }
    93  
    94  func (o *ShellOptions) Run() error {
    95  	config, _, err := o.Kube().LoadConfig()
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	if config == nil || config.Contexts == nil || len(config.Contexts) == 0 {
   101  		return fmt.Errorf("No Kubernetes contexts available! Try create or connect to cluster?")
   102  	}
   103  
   104  	contextNames := []string{}
   105  	for k, v := range config.Contexts {
   106  		if k != "" && v != nil {
   107  			if o.Filter == "" || strings.Index(k, o.Filter) >= 0 {
   108  				contextNames = append(contextNames, k)
   109  			}
   110  		}
   111  	}
   112  	sort.Strings(contextNames)
   113  
   114  	ctxName := ""
   115  	args := o.Args
   116  	if len(args) > 0 {
   117  		ctxName = args[0]
   118  		if util.StringArrayIndex(contextNames, ctxName) < 0 {
   119  			return util.InvalidArg(ctxName, contextNames)
   120  		}
   121  	}
   122  
   123  	if ctxName == "" && !o.BatchMode {
   124  		defaultCtxName := config.CurrentContext
   125  		pick, err := o.PickContext(contextNames, defaultCtxName)
   126  		if err != nil {
   127  			return err
   128  		}
   129  		ctxName = pick
   130  	}
   131  	if ctxName == "" {
   132  		ctxName = config.CurrentContext
   133  	}
   134  	newConfig := *config
   135  	newConfig.CurrentContext = ctxName
   136  
   137  	tmpDirName, err := ioutil.TempDir("", ".jx-shell-")
   138  	if err != nil {
   139  		return err
   140  	}
   141  	tmpConfigFileName := filepath.Join(tmpDirName, "/config")
   142  	err = clientcmd.WriteToFile(newConfig, tmpConfigFileName)
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	fullShell := os.Getenv("SHELL")
   148  	shell := filepath.Base(fullShell)
   149  	if fullShell == "" && runtime.GOOS == "windows" {
   150  		// SHELL is set by git-bash but not cygwin :-(
   151  		shell = "cmd.exe"
   152  	}
   153  
   154  	prompt := o.createNewBashPrompt(os.Getenv("PS1"))
   155  	rcFile := defaultRcFile + "\nexport PS1=" + prompt + "\nexport KUBECONFIG=\"" + tmpConfigFileName + "\"\n"
   156  	tmpRCFileName := tmpDirName + "/.bashrc"
   157  
   158  	if shell == "zsh" {
   159  		prompt = o.createNewZshPrompt(os.Getenv("PS1"))
   160  		rcFile = zshRcFile + "\nexport PS1=" + prompt + "\nexport KUBECONFIG=\"" + tmpConfigFileName + "\"\n"
   161  		tmpRCFileName = tmpDirName + "/.zshrc"
   162  	}
   163  	err = ioutil.WriteFile(tmpRCFileName, []byte(rcFile), util.DefaultWritePermissions)
   164  	if err != nil {
   165  		return err
   166  	}
   167  
   168  	info := util.ColorInfo
   169  	log.Logger().Infof("Creating a new shell using the Kubernetes context %s", info(ctxName))
   170  	if shell != "cmd.exe" {
   171  		log.Logger().Infof("Shell RC file is %s\n", tmpRCFileName)
   172  	}
   173  	log.Logger().Infof("All changes to the Kubernetes context like changing environment, namespace or context will be local to this shell")
   174  	log.Logger().Infof("To return to the global context use the command: exit\n")
   175  
   176  	e := exec.Command(shell, "-rcfile", tmpRCFileName, "-i")
   177  	if shell == "zsh" {
   178  		env := os.Environ()
   179  		env = append(env, fmt.Sprintf("ZDOTDIR=%s", tmpDirName))
   180  		e = exec.Command(shell, "-i")
   181  		e.Env = env
   182  	} else if shell == "cmd.exe" {
   183  		env := os.Environ()
   184  		env = append(env, fmt.Sprintf("KUBECONFIG=%s", tmpConfigFileName))
   185  		e = exec.Command(shell)
   186  		e.Env = env
   187  	}
   188  
   189  	e.Stdout = o.Out
   190  	e.Stderr = o.Err
   191  	e.Stdin = os.Stdin
   192  	err = e.Run()
   193  	if deleteError := os.RemoveAll(tmpDirName); deleteError != nil {
   194  		panic(err)
   195  	}
   196  	return err
   197  
   198  }
   199  
   200  func (o *ShellOptions) PickContext(names []string, defaultValue string) (string, error) {
   201  	surveyOpts := survey.WithStdio(o.In, o.Out, o.Err)
   202  	if len(names) == 0 {
   203  		return "", nil
   204  	}
   205  	if len(names) == 1 {
   206  		return names[0], nil
   207  	}
   208  	name := ""
   209  	prompt := &survey.Select{
   210  		Message: "Change Kubernetes context:",
   211  		Options: names,
   212  		Default: defaultValue,
   213  	}
   214  	err := survey.AskOne(prompt, &name, nil, surveyOpts)
   215  	return name, err
   216  }
   217  
   218  func (o *ShellOptions) createNewBashPrompt(prompt string) string {
   219  	if prompt == "" {
   220  		return "'[\\u@\\h \\W $(jx prompt) ]\\$ '"
   221  	}
   222  	if strings.Contains(prompt, "jx prompt") {
   223  		return prompt
   224  	}
   225  	if prompt[0] == '"' {
   226  		return prompt[0:1] + "$(jx prompt) " + prompt[1:]
   227  	}
   228  	if prompt[0] == '\'' {
   229  		return prompt[0:1] + "$(jx prompt) " + prompt[1:]
   230  	}
   231  	return "'$(jx prompt) " + prompt + "'"
   232  }
   233  
   234  func (o *ShellOptions) createNewZshPrompt(prompt string) string {
   235  	if prompt == "" {
   236  		return "'[$(jx prompt) %n@%m %c]\\$ '"
   237  	}
   238  	if strings.Contains(prompt, "jx prompt") {
   239  		return prompt
   240  	}
   241  	if prompt[0] == '"' {
   242  		return prompt[0:1] + "$(jx prompt) " + prompt[1:]
   243  	}
   244  	if prompt[0] == '\'' {
   245  		return prompt[0:1] + "$(jx prompt) " + prompt[1:]
   246  	}
   247  	return "'$(jx prompt) " + prompt + "'"
   248  }