github.com/jenkins-x/jx/v2@v2.1.155/pkg/cmd/cmd.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors & The Jenkins X Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package cmd 18 19 import ( 20 "fmt" 21 "io" 22 "io/ioutil" 23 "os" 24 "os/exec" 25 "path/filepath" 26 "runtime" 27 "strconv" 28 "strings" 29 "syscall" 30 31 "github.com/jenkins-x/jx/v2/pkg/cmd/deprecation" 32 "github.com/jenkins-x/jx/v2/pkg/cmd/experimental" 33 "github.com/jenkins-x/jx/v2/pkg/cmd/profile" 34 "github.com/jenkins-x/jx/v2/pkg/cmd/ui" 35 36 version2 "github.com/jenkins-x/jx/v2/pkg/cmd/version" 37 38 "github.com/spf13/viper" 39 40 "github.com/jenkins-x/jx/v2/pkg/cmd/boot" 41 "github.com/jenkins-x/jx/v2/pkg/cmd/compliance" 42 "github.com/jenkins-x/jx/v2/pkg/cmd/controller" 43 "github.com/jenkins-x/jx/v2/pkg/cmd/create" 44 "github.com/jenkins-x/jx/v2/pkg/cmd/deletecmd" 45 "github.com/jenkins-x/jx/v2/pkg/cmd/edit" 46 "github.com/jenkins-x/jx/v2/pkg/cmd/gc" 47 "github.com/jenkins-x/jx/v2/pkg/cmd/get" 48 "github.com/jenkins-x/jx/v2/pkg/cmd/importcmd" 49 "github.com/jenkins-x/jx/v2/pkg/cmd/initcmd" 50 "github.com/jenkins-x/jx/v2/pkg/cmd/preview" 51 "github.com/jenkins-x/jx/v2/pkg/cmd/rsh" 52 "github.com/jenkins-x/jx/v2/pkg/cmd/start" 53 "github.com/jenkins-x/jx/v2/pkg/cmd/stop" 54 "github.com/jenkins-x/jx/v2/pkg/cmd/sync" 55 "github.com/jenkins-x/jx/v2/pkg/cmd/uninstall" 56 "github.com/jenkins-x/jx/v2/pkg/cmd/update" 57 "github.com/jenkins-x/jx/v2/pkg/cmd/upgrade" 58 59 "github.com/jenkins-x/jx/v2/pkg/cmd/add" 60 "github.com/jenkins-x/jx/v2/pkg/cmd/namespace" 61 "github.com/jenkins-x/jx/v2/pkg/cmd/promote" 62 63 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 64 65 "github.com/jenkins-x/jx/v2/pkg/extensions" 66 67 "github.com/jenkins-x/jx-logging/pkg/log" 68 "github.com/jenkins-x/jx/v2/pkg/features" 69 "github.com/jenkins-x/jx/v2/pkg/util" 70 71 "github.com/jenkins-x/jx/v2/pkg/cmd/clients" 72 "github.com/jenkins-x/jx/v2/pkg/cmd/opts" 73 "github.com/jenkins-x/jx/v2/pkg/cmd/templates" 74 "github.com/jenkins-x/jx/v2/pkg/version" 75 "github.com/spf13/cobra" 76 "gopkg.in/AlecAivazis/survey.v1/terminal" 77 ) 78 79 // NewJXCommand creates the `jx` command and its nested children. 80 // args used to determine binary plugin to run can be overridden (does not affect compiled in commands). 81 func NewJXCommand(f clients.Factory, in terminal.FileReader, out terminal.FileWriter, 82 err io.Writer, args []string) *cobra.Command { 83 84 configureViper() 85 rootCommand := &cobra.Command{ 86 Use: "jx", 87 Short: "jx is a command line tool for working with Jenkins X", 88 PersistentPreRun: setLoggingLevel, 89 Run: runHelp, 90 } 91 92 features.Init() 93 94 commonOpts := opts.NewCommonOptionsWithTerm(f, in, out, err) 95 commonOpts.AddBaseFlags(rootCommand) 96 97 addCommands := add.NewCmdAdd(commonOpts) 98 createCommands := create.NewCmdCreate(commonOpts) 99 deleteCommands := deletecmd.NewCmdDelete(commonOpts) 100 101 getCommands := get.NewCmdGet(commonOpts) 102 editCommands := edit.NewCmdEdit(commonOpts) 103 updateCommands := update.NewCmdUpdate(commonOpts) 104 105 installCommands := []*cobra.Command{ 106 profile.NewCmdProfile(commonOpts), 107 boot.NewCmdBoot(commonOpts), 108 create.NewCmdInstall(commonOpts), 109 uninstall.NewCmdUninstall(commonOpts), 110 upgrade.NewCmdUpgrade(commonOpts), 111 } 112 installCommands = append(installCommands, findCommands("cluster", createCommands, deleteCommands)...) 113 installCommands = append(installCommands, findCommands("cluster", updateCommands)...) 114 installCommands = append(installCommands, findCommands("jenkins token", createCommands, deleteCommands)...) 115 installCommands = append(installCommands, initcmd.NewCmdInit(commonOpts)) 116 117 addProjectCommands := []*cobra.Command{ 118 importcmd.NewCmdImport(commonOpts), 119 } 120 addProjectCommands = append(addProjectCommands, findCommands("create spring", createCommands, deleteCommands)...) 121 addProjectCommands = append(addProjectCommands, findCommands("create quickstart", createCommands, deleteCommands)...) 122 123 gitCommands := []*cobra.Command{} 124 gitCommands = append(gitCommands, findCommands("git server", createCommands, deleteCommands)...) 125 gitCommands = append(gitCommands, findCommands("git token", createCommands, deleteCommands)...) 126 gitCommands = append(gitCommands, NewCmdRepo(commonOpts)) 127 128 addonCommands := []*cobra.Command{} 129 addonCommands = append(addonCommands, findCommands("addon", createCommands, deleteCommands)...) 130 addonCommands = append(addonCommands, findCommands("app", createCommands, deleteCommands, addCommands)...) 131 132 environmentsCommands := []*cobra.Command{ 133 preview.NewCmdPreview(commonOpts), 134 promote.NewCmdPromote(commonOpts), 135 } 136 environmentsCommands = append(environmentsCommands, findCommands("environment", createCommands, deleteCommands, editCommands, getCommands)...) 137 138 groups := templates.CommandGroups{ 139 { 140 Message: "Installing:", 141 Commands: installCommands, 142 }, 143 { 144 Message: "Adding Projects to Jenkins X:", 145 Commands: addProjectCommands, 146 }, 147 { 148 Message: "Apps:", 149 Commands: addonCommands, 150 }, 151 { 152 Message: "Git:", 153 Commands: gitCommands, 154 }, 155 { 156 Message: "Working with Kubernetes:", 157 Commands: []*cobra.Command{ 158 compliance.NewCompliance(commonOpts), 159 NewCmdCompletion(commonOpts), 160 NewCmdContext(commonOpts), 161 NewCmdEnvironment(commonOpts), 162 NewCmdTeam(commonOpts), 163 namespace.NewCmdNamespace(commonOpts), 164 NewCmdPrompt(commonOpts), 165 NewCmdScan(commonOpts), 166 NewCmdShell(commonOpts), 167 NewCmdStatus(commonOpts), 168 }, 169 }, 170 { 171 Message: "Working with Applications:", 172 Commands: []*cobra.Command{ 173 NewCmdLogs(commonOpts), 174 NewCmdOpen(commonOpts), 175 rsh.NewCmdRsh(commonOpts), 176 sync.NewCmdSync(commonOpts), 177 }, 178 }, 179 { 180 Message: "Working with Environments:", 181 Commands: environmentsCommands, 182 }, 183 { 184 Message: "Working with Jenkins X resources:", 185 Commands: []*cobra.Command{ 186 getCommands, 187 editCommands, 188 createCommands, 189 updateCommands, 190 deleteCommands, 191 addCommands, 192 start.NewCmdStart(commonOpts), 193 stop.NewCmdStop(commonOpts), 194 }, 195 }, 196 { 197 Message: "Jenkins X Pipeline Commands:", 198 Commands: []*cobra.Command{ 199 NewCmdStep(commonOpts), 200 }, 201 }, 202 { 203 Message: "Jenkins X services:", 204 Commands: []*cobra.Command{ 205 controller.NewCmdController(commonOpts), 206 gc.NewCmdGC(commonOpts), 207 }, 208 }, 209 { 210 Message: "Working with Jenkins X UI:", 211 Commands: []*cobra.Command{ 212 ui.NewCmdUI(commonOpts), 213 }, 214 }, 215 } 216 217 groups.Add(rootCommand) 218 219 filters := []string{"options"} 220 221 getPluginCommandGroups := func() (templates.PluginCommandGroups, bool) { 222 verifier := &extensions.CommandOverrideVerifier{ 223 Root: rootCommand, 224 SeenPlugins: make(map[string]string, 0), 225 } 226 pluginCommandGroups, managedPluginsEnabled, err := commonOpts.GetPluginCommandGroups(verifier) 227 if err != nil { 228 log.Logger().Errorf("%v", err) 229 } 230 return pluginCommandGroups, managedPluginsEnabled 231 } 232 templates.ActsAsRootCommand(rootCommand, filters, getPluginCommandGroups, groups...) 233 rootCommand.AddCommand(NewCmdDocs(commonOpts)) 234 rootCommand.AddCommand(version2.NewCmdVersion(commonOpts)) 235 rootCommand.Version = version.GetVersion() 236 rootCommand.SetVersionTemplate("{{printf .Version}}\n Deprecated will be removed on July 1, 2020. Please use version instead\n") 237 rootCommand.AddCommand(NewCmdOptions(out)) 238 rootCommand.AddCommand(NewCmdDiagnose(commonOpts)) 239 240 // Mark the deprecated commands 241 deprecation.DeprecateCommands(rootCommand) 242 243 // Mark the experimental commands 244 experimental.AlphaCommands(rootCommand) 245 experimental.BetaCommands(rootCommand) 246 247 managedPlugins := &managedPluginHandler{ 248 CommonOptions: commonOpts, 249 } 250 localPlugins := &localPluginHandler{} 251 252 if len(args) == 0 { 253 args = os.Args 254 } 255 if len(args) > 1 { 256 cmdPathPieces := args[1:] 257 258 // only look for suitable executables if 259 // the specified command does not already exist 260 if _, _, err := rootCommand.Find(cmdPathPieces); err != nil { 261 if _, managedPluginsEnabled := getPluginCommandGroups(); managedPluginsEnabled { 262 if err := handleEndpointExtensions(managedPlugins, cmdPathPieces); err != nil { 263 log.Logger().Errorf("%v", err) 264 os.Exit(1) 265 } 266 } else { 267 if err := handleEndpointExtensions(localPlugins, cmdPathPieces); err != nil { 268 log.Logger().Errorf("%v", err) 269 os.Exit(1) 270 } 271 } 272 273 } 274 } 275 return rootCommand 276 } 277 278 func configureViper() { 279 replacer := strings.NewReplacer("-", "_") 280 viper.SetEnvKeyReplacer(replacer) 281 } 282 283 func findCommands(subCommand string, commands ...*cobra.Command) []*cobra.Command { 284 answer := []*cobra.Command{} 285 for _, parent := range commands { 286 for _, c := range parent.Commands() { 287 if commandHasParentName(c, subCommand) { 288 answer = append(answer, c) 289 } else { 290 childCommands := findCommands(subCommand, c) 291 if len(childCommands) > 0 { 292 answer = append(answer, childCommands...) 293 } 294 } 295 } 296 } 297 return answer 298 } 299 300 func commandHasParentName(command *cobra.Command, name string) bool { 301 path := fullPath(command) 302 return strings.Contains(path, name) 303 } 304 305 func fullPath(command *cobra.Command) string { 306 name := command.Name() 307 parent := command.Parent() 308 if parent != nil { 309 return fullPath(parent) + " " + name 310 } 311 return name 312 } 313 314 func setLoggingLevel(cmd *cobra.Command, args []string) { 315 verbose, err := strconv.ParseBool(cmd.Flag(opts.OptionVerbose).Value.String()) 316 if err != nil { 317 log.Logger().Errorf("Unable to check if the verbose flag is set") 318 } 319 320 level := os.Getenv("JX_LOG_LEVEL") 321 if level != "" { 322 if verbose { 323 log.Logger().Trace("The JX_LOG_LEVEL environment variable took precedence over the verbose flag") 324 } 325 326 err := log.SetLevel(level) 327 if err != nil { 328 log.Logger().Errorf("Unable to set log level to %s", level) 329 } 330 } else { 331 if verbose { 332 err := log.SetLevel("debug") 333 if err != nil { 334 log.Logger().Errorf("Unable to set log level to debug") 335 } 336 } else { 337 err := log.SetLevel("info") 338 if err != nil { 339 log.Logger().Errorf("Unable to set log level to info") 340 } 341 } 342 } 343 } 344 345 func runHelp(cmd *cobra.Command, args []string) { 346 cmd.Help() //nolint:errcheck 347 } 348 349 // PluginHandler is capable of parsing command line arguments 350 // and performing executable filename lookups to search 351 // for valid plugin files, and execute found plugins. 352 type PluginHandler interface { 353 // Lookup receives a potential filename and returns 354 // a full or relative path to an executable, if one 355 // exists at the given filename, or an error. 356 Lookup(filename string) (string, error) 357 // Execute receives an executable's filepath, a slice 358 // of arguments, and a slice of environment variables 359 // to relay to the executable. 360 Execute(executablePath string, cmdArgs, environment []string) error 361 } 362 363 type managedPluginHandler struct { 364 *opts.CommonOptions 365 localPluginHandler 366 } 367 368 // Lookup implements PluginHandler 369 func (h *managedPluginHandler) Lookup(filename string) (string, error) { 370 jxClient, ns, err := h.JXClientAndDevNamespace() 371 if err != nil { 372 return "", err 373 } 374 375 possibles, err := jxClient.JenkinsV1().Plugins(ns).List(metav1.ListOptions{ 376 LabelSelector: fmt.Sprintf("%s=%s", extensions.PluginCommandLabel, filename), 377 }) 378 if err != nil { 379 return "", err 380 } 381 if len(possibles.Items) > 0 { 382 found := possibles.Items[0] 383 if len(possibles.Items) > 1 { 384 // There is a warning about this when you install extensions as well 385 log.Logger().Warnf("More than one plugin installed for %s by apps. Selecting the one installed by %s at random.", 386 filename, found.Name) 387 388 } 389 return extensions.EnsurePluginInstalled(found) 390 } 391 return h.localPluginHandler.Lookup(filename) 392 } 393 394 // Execute implements PluginHandler 395 func (h *managedPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error { 396 return h.localPluginHandler.Execute(executablePath, cmdArgs, environment) 397 } 398 399 type localPluginHandler struct{} 400 401 // Lookup implements PluginHandler 402 func (h *localPluginHandler) Lookup(filename string) (string, error) { 403 // if on Windows, append the "exe" extension 404 // to the filename that we are looking up. 405 if runtime.GOOS == "windows" { 406 filename = filename + ".exe" 407 } 408 409 return exec.LookPath(filename) 410 } 411 412 // Execute implements PluginHandler 413 func (h *localPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error { 414 return syscall.Exec(executablePath, cmdArgs, environment) 415 } 416 417 func handleEndpointExtensions(pluginHandler PluginHandler, cmdArgs []string) error { 418 remainingArgs := []string{} // all "non-flag" arguments 419 420 for idx := range cmdArgs { 421 if strings.HasPrefix(cmdArgs[idx], "-") { 422 break 423 } 424 remainingArgs = append(remainingArgs, strings.Replace(cmdArgs[idx], "-", "_", -1)) 425 } 426 427 foundBinaryPath := "" 428 429 pluginDir, err := util.PluginBinDir("jx") 430 if err != nil { 431 log.Logger().Debugf("failed to find plugin dir %s", err.Error()) 432 } 433 434 // attempt to find binary, starting at longest possible name with given cmdArgs 435 for len(remainingArgs) > 0 { 436 commandName := fmt.Sprintf("jx-%s", strings.Join(remainingArgs, "-")) 437 path, err := pluginHandler.Lookup(commandName) 438 if err != nil || len(path) == 0 { 439 // lets see if we have previously downloaded this binary plugin 440 path = FindPluginBinary(pluginDir, commandName) 441 if path != "" { 442 foundBinaryPath = path 443 break 444 } 445 446 /* Usually "executable file not found in $PATH", spams output of jx help subcommand: 447 if err != nil { 448 log.Logger().Errorf("Error installing plugin for command %s. %v\n", remainingArgs, err) 449 } 450 */ 451 remainingArgs = remainingArgs[:len(remainingArgs)-1] 452 continue 453 } 454 455 foundBinaryPath = path 456 break 457 } 458 459 if len(foundBinaryPath) == 0 { 460 return nil 461 } 462 463 // invoke cmd binary relaying the current environment and args given 464 // remainingArgs will always have at least one element. 465 // execve will make remainingArgs[0] the "binary name". 466 if err := pluginHandler.Execute(foundBinaryPath, append([]string{foundBinaryPath}, cmdArgs[len(remainingArgs):]...), os.Environ()); err != nil { 467 return err 468 } 469 470 return nil 471 } 472 473 // FindPluginBinary tries to find the jx-foo binary plugin in the plugins dir `~/.jx/plugins/jx/bin` dir ` 474 func FindPluginBinary(pluginDir string, commandName string) string { 475 if pluginDir != "" { 476 files, err := ioutil.ReadDir(pluginDir) 477 if err != nil { 478 log.Logger().Debugf("failed to read plugin dir %s", err.Error()) 479 } else { 480 prefix := commandName + "-" 481 for _, f := range files { 482 name := f.Name() 483 if strings.HasPrefix(name, prefix) { 484 path := filepath.Join(pluginDir, name) 485 log.Logger().Debugf("found plugin %s at %s", commandName, path) 486 return path 487 } 488 } 489 } 490 } 491 return "" 492 }