1/* 2Copyright 2014 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package cmd 18 19import ( 20 "flag" 21 "fmt" 22 "io" 23 "net/http" 24 "os" 25 "os/exec" 26 "runtime" 27 "strings" 28 "syscall" 29 30 "github.com/spf13/cobra" 31 32 "k8s.io/client-go/rest" 33 "k8s.io/client-go/tools/clientcmd" 34 cliflag "k8s.io/component-base/cli/flag" 35 "k8s.io/klog/v2" 36 "k8s.io/kubectl/pkg/cmd/annotate" 37 "k8s.io/kubectl/pkg/cmd/apiresources" 38 "k8s.io/kubectl/pkg/cmd/apply" 39 "k8s.io/kubectl/pkg/cmd/attach" 40 "k8s.io/kubectl/pkg/cmd/auth" 41 "k8s.io/kubectl/pkg/cmd/autoscale" 42 "k8s.io/kubectl/pkg/cmd/certificates" 43 "k8s.io/kubectl/pkg/cmd/clusterinfo" 44 "k8s.io/kubectl/pkg/cmd/completion" 45 cmdconfig "k8s.io/kubectl/pkg/cmd/config" 46 "k8s.io/kubectl/pkg/cmd/cp" 47 "k8s.io/kubectl/pkg/cmd/create" 48 "k8s.io/kubectl/pkg/cmd/debug" 49 "k8s.io/kubectl/pkg/cmd/delete" 50 "k8s.io/kubectl/pkg/cmd/describe" 51 "k8s.io/kubectl/pkg/cmd/diff" 52 "k8s.io/kubectl/pkg/cmd/drain" 53 "k8s.io/kubectl/pkg/cmd/edit" 54 cmdexec "k8s.io/kubectl/pkg/cmd/exec" 55 "k8s.io/kubectl/pkg/cmd/explain" 56 "k8s.io/kubectl/pkg/cmd/expose" 57 "k8s.io/kubectl/pkg/cmd/get" 58 "k8s.io/kubectl/pkg/cmd/label" 59 "k8s.io/kubectl/pkg/cmd/logs" 60 "k8s.io/kubectl/pkg/cmd/options" 61 "k8s.io/kubectl/pkg/cmd/patch" 62 "k8s.io/kubectl/pkg/cmd/plugin" 63 "k8s.io/kubectl/pkg/cmd/portforward" 64 "k8s.io/kubectl/pkg/cmd/proxy" 65 "k8s.io/kubectl/pkg/cmd/replace" 66 "k8s.io/kubectl/pkg/cmd/rollout" 67 "k8s.io/kubectl/pkg/cmd/run" 68 "k8s.io/kubectl/pkg/cmd/scale" 69 "k8s.io/kubectl/pkg/cmd/set" 70 "k8s.io/kubectl/pkg/cmd/taint" 71 "k8s.io/kubectl/pkg/cmd/top" 72 cmdutil "k8s.io/kubectl/pkg/cmd/util" 73 "k8s.io/kubectl/pkg/cmd/version" 74 "k8s.io/kubectl/pkg/cmd/wait" 75 "k8s.io/kubectl/pkg/util" 76 "k8s.io/kubectl/pkg/util/i18n" 77 "k8s.io/kubectl/pkg/util/templates" 78 "k8s.io/kubectl/pkg/util/term" 79 80 "k8s.io/cli-runtime/pkg/genericclioptions" 81 "k8s.io/kubectl/pkg/cmd/kustomize" 82) 83 84const kubectlCmdHeaders = "KUBECTL_COMMAND_HEADERS" 85 86// NewDefaultKubectlCommand creates the `kubectl` command with default arguments 87func NewDefaultKubectlCommand() *cobra.Command { 88 return NewDefaultKubectlCommandWithArgs(NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes), os.Args, os.Stdin, os.Stdout, os.Stderr) 89} 90 91// NewDefaultKubectlCommandWithArgs creates the `kubectl` command with arguments 92func NewDefaultKubectlCommandWithArgs(pluginHandler PluginHandler, args []string, in io.Reader, out, errout io.Writer) *cobra.Command { 93 cmd := NewKubectlCommand(in, out, errout) 94 95 if pluginHandler == nil { 96 return cmd 97 } 98 99 if len(args) > 1 { 100 cmdPathPieces := args[1:] 101 102 // only look for suitable extension executables if 103 // the specified command does not already exist 104 if _, _, err := cmd.Find(cmdPathPieces); err != nil { 105 if err := HandlePluginCommand(pluginHandler, cmdPathPieces); err != nil { 106 fmt.Fprintf(errout, "Error: %v\n", err) 107 os.Exit(1) 108 } 109 } 110 } 111 112 return cmd 113} 114 115// PluginHandler is capable of parsing command line arguments 116// and performing executable filename lookups to search 117// for valid plugin files, and execute found plugins. 118type PluginHandler interface { 119 // exists at the given filename, or a boolean false. 120 // Lookup will iterate over a list of given prefixes 121 // in order to recognize valid plugin filenames. 122 // The first filepath to match a prefix is returned. 123 Lookup(filename string) (string, bool) 124 // Execute receives an executable's filepath, a slice 125 // of arguments, and a slice of environment variables 126 // to relay to the executable. 127 Execute(executablePath string, cmdArgs, environment []string) error 128} 129 130// DefaultPluginHandler implements PluginHandler 131type DefaultPluginHandler struct { 132 ValidPrefixes []string 133} 134 135// NewDefaultPluginHandler instantiates the DefaultPluginHandler with a list of 136// given filename prefixes used to identify valid plugin filenames. 137func NewDefaultPluginHandler(validPrefixes []string) *DefaultPluginHandler { 138 return &DefaultPluginHandler{ 139 ValidPrefixes: validPrefixes, 140 } 141} 142 143// Lookup implements PluginHandler 144func (h *DefaultPluginHandler) Lookup(filename string) (string, bool) { 145 for _, prefix := range h.ValidPrefixes { 146 path, err := exec.LookPath(fmt.Sprintf("%s-%s", prefix, filename)) 147 if err != nil || len(path) == 0 { 148 continue 149 } 150 return path, true 151 } 152 153 return "", false 154} 155 156// Execute implements PluginHandler 157func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error { 158 159 // Windows does not support exec syscall. 160 if runtime.GOOS == "windows" { 161 cmd := exec.Command(executablePath, cmdArgs...) 162 cmd.Stdout = os.Stdout 163 cmd.Stderr = os.Stderr 164 cmd.Stdin = os.Stdin 165 cmd.Env = environment 166 err := cmd.Run() 167 if err == nil { 168 os.Exit(0) 169 } 170 return err 171 } 172 173 // invoke cmd binary relaying the environment and args given 174 // append executablePath to cmdArgs, as execve will make first argument the "binary name". 175 return syscall.Exec(executablePath, append([]string{executablePath}, cmdArgs...), environment) 176} 177 178// HandlePluginCommand receives a pluginHandler and command-line arguments and attempts to find 179// a plugin executable on the PATH that satisfies the given arguments. 180func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string) error { 181 var remainingArgs []string // all "non-flag" arguments 182 for _, arg := range cmdArgs { 183 if strings.HasPrefix(arg, "-") { 184 break 185 } 186 remainingArgs = append(remainingArgs, strings.Replace(arg, "-", "_", -1)) 187 } 188 189 if len(remainingArgs) == 0 { 190 // the length of cmdArgs is at least 1 191 return fmt.Errorf("flags cannot be placed before plugin name: %s", cmdArgs[0]) 192 } 193 194 foundBinaryPath := "" 195 196 // attempt to find binary, starting at longest possible name with given cmdArgs 197 for len(remainingArgs) > 0 { 198 path, found := pluginHandler.Lookup(strings.Join(remainingArgs, "-")) 199 if !found { 200 remainingArgs = remainingArgs[:len(remainingArgs)-1] 201 continue 202 } 203 204 foundBinaryPath = path 205 break 206 } 207 208 if len(foundBinaryPath) == 0 { 209 return nil 210 } 211 212 // invoke cmd binary relaying the current environment and args given 213 if err := pluginHandler.Execute(foundBinaryPath, cmdArgs[len(remainingArgs):], os.Environ()); err != nil { 214 return err 215 } 216 217 return nil 218} 219 220// NewKubectlCommand creates the `kubectl` command and its nested children. 221func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command { 222 warningHandler := rest.NewWarningWriter(err, rest.WarningWriterOptions{Deduplicate: true, Color: term.AllowsColorOutput(err)}) 223 warningsAsErrors := false 224 // Parent command to which all subcommands are added. 225 cmds := &cobra.Command{ 226 Use: "kubectl", 227 Short: i18n.T("kubectl controls the Kubernetes cluster manager"), 228 Long: templates.LongDesc(` 229 kubectl controls the Kubernetes cluster manager. 230 231 Find more information at: 232 https://kubernetes.io/docs/reference/kubectl/overview/`), 233 Run: runHelp, 234 // Hook before and after Run initialize and write profiles to disk, 235 // respectively. 236 PersistentPreRunE: func(*cobra.Command, []string) error { 237 rest.SetDefaultWarningHandler(warningHandler) 238 return initProfiling() 239 }, 240 PersistentPostRunE: func(*cobra.Command, []string) error { 241 if err := flushProfiling(); err != nil { 242 return err 243 } 244 if warningsAsErrors { 245 count := warningHandler.WarningCount() 246 switch count { 247 case 0: 248 // no warnings 249 case 1: 250 return fmt.Errorf("%d warning received", count) 251 default: 252 return fmt.Errorf("%d warnings received", count) 253 } 254 } 255 return nil 256 }, 257 } 258 259 flags := cmds.PersistentFlags() 260 flags.SetNormalizeFunc(cliflag.WarnWordSepNormalizeFunc) // Warn for "_" flags 261 262 // Normalize all flags that are coming from other packages or pre-configurations 263 // a.k.a. change all "_" to "-". e.g. glog package 264 flags.SetNormalizeFunc(cliflag.WordSepNormalizeFunc) 265 266 addProfilingFlags(flags) 267 268 flags.BoolVar(&warningsAsErrors, "warnings-as-errors", warningsAsErrors, "Treat warnings received from the server as errors and exit with a non-zero exit code") 269 270 kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() 271 kubeConfigFlags.AddFlags(flags) 272 matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) 273 matchVersionKubeConfigFlags.AddFlags(cmds.PersistentFlags()) 274 // Updates hooks to add kubectl command headers: SIG CLI KEP 859. 275 addCmdHeaderHooks(cmds, kubeConfigFlags) 276 277 cmds.PersistentFlags().AddGoFlagSet(flag.CommandLine) 278 279 f := cmdutil.NewFactory(matchVersionKubeConfigFlags) 280 281 // Sending in 'nil' for the getLanguageFn() results in using 282 // the LANG environment variable. 283 // 284 // TODO: Consider adding a flag or file preference for setting 285 // the language, instead of just loading from the LANG env. variable. 286 i18n.LoadTranslations("kubectl", nil) 287 288 // From this point and forward we get warnings on flags that contain "_" separators 289 cmds.SetGlobalNormalizationFunc(cliflag.WarnWordSepNormalizeFunc) 290 291 ioStreams := genericclioptions.IOStreams{In: in, Out: out, ErrOut: err} 292 293 // Proxy command is incompatible with CommandHeaderRoundTripper, so 294 // clear the WrapConfigFn before running proxy command. 295 proxyCmd := proxy.NewCmdProxy(f, ioStreams) 296 proxyCmd.PreRun = func(cmd *cobra.Command, args []string) { 297 kubeConfigFlags.WrapConfigFn = nil 298 } 299 groups := templates.CommandGroups{ 300 { 301 Message: "Basic Commands (Beginner):", 302 Commands: []*cobra.Command{ 303 create.NewCmdCreate(f, ioStreams), 304 expose.NewCmdExposeService(f, ioStreams), 305 run.NewCmdRun(f, ioStreams), 306 set.NewCmdSet(f, ioStreams), 307 }, 308 }, 309 { 310 Message: "Basic Commands (Intermediate):", 311 Commands: []*cobra.Command{ 312 explain.NewCmdExplain("kubectl", f, ioStreams), 313 get.NewCmdGet("kubectl", f, ioStreams), 314 edit.NewCmdEdit(f, ioStreams), 315 delete.NewCmdDelete(f, ioStreams), 316 }, 317 }, 318 { 319 Message: "Deploy Commands:", 320 Commands: []*cobra.Command{ 321 rollout.NewCmdRollout(f, ioStreams), 322 scale.NewCmdScale(f, ioStreams), 323 autoscale.NewCmdAutoscale(f, ioStreams), 324 }, 325 }, 326 { 327 Message: "Cluster Management Commands:", 328 Commands: []*cobra.Command{ 329 certificates.NewCmdCertificate(f, ioStreams), 330 clusterinfo.NewCmdClusterInfo(f, ioStreams), 331 top.NewCmdTop(f, ioStreams), 332 drain.NewCmdCordon(f, ioStreams), 333 drain.NewCmdUncordon(f, ioStreams), 334 drain.NewCmdDrain(f, ioStreams), 335 taint.NewCmdTaint(f, ioStreams), 336 }, 337 }, 338 { 339 Message: "Troubleshooting and Debugging Commands:", 340 Commands: []*cobra.Command{ 341 describe.NewCmdDescribe("kubectl", f, ioStreams), 342 logs.NewCmdLogs(f, ioStreams), 343 attach.NewCmdAttach(f, ioStreams), 344 cmdexec.NewCmdExec(f, ioStreams), 345 portforward.NewCmdPortForward(f, ioStreams), 346 proxyCmd, 347 cp.NewCmdCp(f, ioStreams), 348 auth.NewCmdAuth(f, ioStreams), 349 debug.NewCmdDebug(f, ioStreams), 350 }, 351 }, 352 { 353 Message: "Advanced Commands:", 354 Commands: []*cobra.Command{ 355 diff.NewCmdDiff(f, ioStreams), 356 apply.NewCmdApply("kubectl", f, ioStreams), 357 patch.NewCmdPatch(f, ioStreams), 358 replace.NewCmdReplace(f, ioStreams), 359 wait.NewCmdWait(f, ioStreams), 360 kustomize.NewCmdKustomize(ioStreams), 361 }, 362 }, 363 { 364 Message: "Settings Commands:", 365 Commands: []*cobra.Command{ 366 label.NewCmdLabel(f, ioStreams), 367 annotate.NewCmdAnnotate("kubectl", f, ioStreams), 368 completion.NewCmdCompletion(ioStreams.Out, ""), 369 }, 370 }, 371 } 372 groups.Add(cmds) 373 374 filters := []string{"options"} 375 376 // Hide the "alpha" subcommand if there are no alpha commands in this build. 377 alpha := NewCmdAlpha(ioStreams) 378 if !alpha.HasSubCommands() { 379 filters = append(filters, alpha.Name()) 380 } 381 382 templates.ActsAsRootCommand(cmds, filters, groups...) 383 384 util.SetFactoryForCompletion(f) 385 registerCompletionFuncForGlobalFlags(cmds, f) 386 387 cmds.AddCommand(alpha) 388 cmds.AddCommand(cmdconfig.NewCmdConfig(clientcmd.NewDefaultPathOptions(), ioStreams)) 389 cmds.AddCommand(plugin.NewCmdPlugin(ioStreams)) 390 cmds.AddCommand(version.NewCmdVersion(f, ioStreams)) 391 cmds.AddCommand(apiresources.NewCmdAPIVersions(f, ioStreams)) 392 cmds.AddCommand(apiresources.NewCmdAPIResources(f, ioStreams)) 393 cmds.AddCommand(options.NewCmdOptions(ioStreams.Out)) 394 395 return cmds 396} 397 398// addCmdHeaderHooks performs updates on two hooks: 399// 1) Modifies the passed "cmds" persistent pre-run function to parse command headers. 400// These headers will be subsequently added as X-headers to every 401// REST call. 402// 2) Adds CommandHeaderRoundTripper as a wrapper around the standard 403// RoundTripper. CommandHeaderRoundTripper adds X-Headers then delegates 404// to standard RoundTripper. 405// For beta, these hooks are updated unless the KUBECTL_COMMAND_HEADERS environment variable 406// is set, and the value of the env var is false (or zero). 407// See SIG CLI KEP 859 for more information: 408// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/859-kubectl-headers 409func addCmdHeaderHooks(cmds *cobra.Command, kubeConfigFlags *genericclioptions.ConfigFlags) { 410 // If the feature gate env var is set to "false", then do no add kubectl command headers. 411 if value, exists := os.LookupEnv(kubectlCmdHeaders); exists { 412 if value == "false" || value == "0" { 413 klog.V(5).Infoln("kubectl command headers turned off") 414 return 415 } 416 } 417 klog.V(5).Infoln("kubectl command headers turned on") 418 crt := &genericclioptions.CommandHeaderRoundTripper{} 419 existingPreRunE := cmds.PersistentPreRunE 420 // Add command parsing to the existing persistent pre-run function. 421 cmds.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { 422 crt.ParseCommandHeaders(cmd, args) 423 return existingPreRunE(cmd, args) 424 } 425 // Wraps CommandHeaderRoundTripper around standard RoundTripper. 426 kubeConfigFlags.WrapConfigFn = func(c *rest.Config) *rest.Config { 427 c.Wrap(func(rt http.RoundTripper) http.RoundTripper { 428 crt.Delegate = rt 429 return crt 430 }) 431 return c 432 } 433} 434 435func runHelp(cmd *cobra.Command, args []string) { 436 cmd.Help() 437} 438 439func registerCompletionFuncForGlobalFlags(cmd *cobra.Command, f cmdutil.Factory) { 440 cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( 441 "namespace", 442 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 443 return get.CompGetResource(f, cmd, "namespace", toComplete), cobra.ShellCompDirectiveNoFileComp 444 })) 445 cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( 446 "context", 447 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 448 return util.ListContextsInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp 449 })) 450 cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( 451 "cluster", 452 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 453 return util.ListClustersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp 454 })) 455 cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( 456 "user", 457 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 458 return util.ListUsersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp 459 })) 460} 461