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