github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/shell.go (about) 1 package cmd 2 3 import ( 4 "fmt" 5 "path/filepath" 6 "runtime" 7 8 "github.com/olli-ai/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/olli-ai/jx/v2/pkg/cmd/opts" 15 "github.com/olli-ai/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/olli-ai/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 }