diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index a7d998e7d..5f484d1f0 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -920,11 +920,11 @@ }, { "ImportPath": "k8s.io/api", - "Rev": "5dbd1534041f" + "Rev": "88ad409c1fcc" }, { "ImportPath": "k8s.io/apimachinery", - "Rev": "2afe1df57c03" + "Rev": "000b5f4f8623" }, { "ImportPath": "k8s.io/cli-runtime", @@ -932,7 +932,7 @@ }, { "ImportPath": "k8s.io/client-go", - "Rev": "bef66adadf9a" + "Rev": "30548acd0a9e" }, { "ImportPath": "k8s.io/code-generator", @@ -944,7 +944,7 @@ }, { "ImportPath": "k8s.io/component-helpers", - "Rev": "7624ab466412" + "Rev": "2da294c6f4f4" }, { "ImportPath": "k8s.io/gengo", diff --git a/go.mod b/go.mod index e439ae6cc..1dfe78c48 100644 --- a/go.mod +++ b/go.mod @@ -34,12 +34,12 @@ require ( github.com/stretchr/testify v1.4.0 golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 gopkg.in/yaml.v2 v2.2.8 - k8s.io/api v0.0.0-20201111082238-5dbd1534041f - k8s.io/apimachinery v0.0.0-20201109202106-2afe1df57c03 + k8s.io/api v0.0.0-20201112122247-88ad409c1fcc + k8s.io/apimachinery v0.0.0-20201112162105-000b5f4f8623 k8s.io/cli-runtime v0.0.0-20201109203813-022e66e1905f - k8s.io/client-go v0.0.0-20201109162515-bef66adadf9a + k8s.io/client-go v0.0.0-20201112202528-30548acd0a9e k8s.io/component-base v0.0.0-20201110162559-92d83a5bfee5 - k8s.io/component-helpers v0.0.0-20201109163048-7624ab466412 + k8s.io/component-helpers v0.0.0-20201111122703-2da294c6f4f4 k8s.io/klog/v2 v2.4.0 k8s.io/kube-openapi v0.0.0-20201107163737-74b467f3a622 k8s.io/metrics v0.0.0-20201109164148-9446cab9abd5 @@ -49,12 +49,12 @@ require ( ) replace ( - k8s.io/api => k8s.io/api v0.0.0-20201111082238-5dbd1534041f - k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20201109202106-2afe1df57c03 + k8s.io/api => k8s.io/api v0.0.0-20201112122247-88ad409c1fcc + k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20201112162105-000b5f4f8623 k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20201109203813-022e66e1905f - k8s.io/client-go => k8s.io/client-go v0.0.0-20201109162515-bef66adadf9a + k8s.io/client-go => k8s.io/client-go v0.0.0-20201112202528-30548acd0a9e k8s.io/code-generator => k8s.io/code-generator v0.0.0-20201109161924-ed440de59c89 k8s.io/component-base => k8s.io/component-base v0.0.0-20201110162559-92d83a5bfee5 - k8s.io/component-helpers => k8s.io/component-helpers v0.0.0-20201109163048-7624ab466412 + k8s.io/component-helpers => k8s.io/component-helpers v0.0.0-20201111122703-2da294c6f4f4 k8s.io/metrics => k8s.io/metrics v0.0.0-20201109164148-9446cab9abd5 ) diff --git a/go.sum b/go.sum index bf406baf3..d46bd5681 100644 --- a/go.sum +++ b/go.sum @@ -551,13 +551,13 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.0.0-20201111082238-5dbd1534041f/go.mod h1:kdpOjtJg1fLz8kIoeBe0Ohm3dngnl37spUZUciyX7Ho= -k8s.io/apimachinery v0.0.0-20201109202106-2afe1df57c03/go.mod h1:C4HVOTZCaKwcNDaxwQoXe3Zk6BLT5jNivqfdcniEtuY= +k8s.io/api v0.0.0-20201112122247-88ad409c1fcc/go.mod h1:kdpOjtJg1fLz8kIoeBe0Ohm3dngnl37spUZUciyX7Ho= +k8s.io/apimachinery v0.0.0-20201112162105-000b5f4f8623/go.mod h1:C4HVOTZCaKwcNDaxwQoXe3Zk6BLT5jNivqfdcniEtuY= k8s.io/cli-runtime v0.0.0-20201109203813-022e66e1905f/go.mod h1:5+tQFFC3RJtGTXnKO8U3JFHHGfj6+kt/EhhAY40eDSA= -k8s.io/client-go v0.0.0-20201109162515-bef66adadf9a/go.mod h1:9VsPQPe2J8dbpOmXX5TunLm7OmOxj+v+YY3F0EHC2yw= +k8s.io/client-go v0.0.0-20201112202528-30548acd0a9e/go.mod h1:KkY4/tSKSJeHkCItTqOpUcLiN4HNZDViOeW+hKwhU3k= k8s.io/code-generator v0.0.0-20201109161924-ed440de59c89/go.mod h1:wUyo2szs8ByJHpQTch9zvCFzf2nVOWUqIgaJeNbKmv4= k8s.io/component-base v0.0.0-20201110162559-92d83a5bfee5/go.mod h1:tt1vbPuUzK9xrCBhF3WvXUnJR9dWo1n/LKtbrNDDxVk= -k8s.io/component-helpers v0.0.0-20201109163048-7624ab466412/go.mod h1:zRJ/d+VRz0xzlfEvLF5WGtgphKa/UpRq2NW5NvDnj50= +k8s.io/component-helpers v0.0.0-20201111122703-2da294c6f4f4/go.mod h1:plbwE6+4Km4k3NVzwkhpvbgpUM78i5jsvoJqV7BtyLI= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go new file mode 100644 index 000000000..999e0e4cb --- /dev/null +++ b/pkg/cmd/cmd.go @@ -0,0 +1,617 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "flag" + "fmt" + "io" + "os" + "os/exec" + "runtime" + "strings" + "syscall" + + "github.com/spf13/cobra" + + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + cliflag "k8s.io/component-base/cli/flag" + "k8s.io/kubectl/pkg/cmd/annotate" + "k8s.io/kubectl/pkg/cmd/apiresources" + "k8s.io/kubectl/pkg/cmd/apply" + "k8s.io/kubectl/pkg/cmd/attach" + "k8s.io/kubectl/pkg/cmd/auth" + "k8s.io/kubectl/pkg/cmd/autoscale" + "k8s.io/kubectl/pkg/cmd/certificates" + "k8s.io/kubectl/pkg/cmd/clusterinfo" + "k8s.io/kubectl/pkg/cmd/completion" + cmdconfig "k8s.io/kubectl/pkg/cmd/config" + "k8s.io/kubectl/pkg/cmd/cp" + "k8s.io/kubectl/pkg/cmd/create" + "k8s.io/kubectl/pkg/cmd/debug" + "k8s.io/kubectl/pkg/cmd/delete" + "k8s.io/kubectl/pkg/cmd/describe" + "k8s.io/kubectl/pkg/cmd/diff" + "k8s.io/kubectl/pkg/cmd/drain" + "k8s.io/kubectl/pkg/cmd/edit" + cmdexec "k8s.io/kubectl/pkg/cmd/exec" + "k8s.io/kubectl/pkg/cmd/explain" + "k8s.io/kubectl/pkg/cmd/expose" + "k8s.io/kubectl/pkg/cmd/get" + "k8s.io/kubectl/pkg/cmd/label" + "k8s.io/kubectl/pkg/cmd/logs" + "k8s.io/kubectl/pkg/cmd/options" + "k8s.io/kubectl/pkg/cmd/patch" + "k8s.io/kubectl/pkg/cmd/plugin" + "k8s.io/kubectl/pkg/cmd/portforward" + "k8s.io/kubectl/pkg/cmd/proxy" + "k8s.io/kubectl/pkg/cmd/replace" + "k8s.io/kubectl/pkg/cmd/rollout" + "k8s.io/kubectl/pkg/cmd/run" + "k8s.io/kubectl/pkg/cmd/scale" + "k8s.io/kubectl/pkg/cmd/set" + "k8s.io/kubectl/pkg/cmd/taint" + "k8s.io/kubectl/pkg/cmd/top" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/cmd/version" + "k8s.io/kubectl/pkg/cmd/wait" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" + "k8s.io/kubectl/pkg/util/term" + + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/kubectl/pkg/cmd/kustomize" +) + +const ( + bashCompletionFunc = `# call kubectl get $1, +__kubectl_debug_out() +{ + local cmd="$1" + __kubectl_debug "${FUNCNAME[1]}: get completion by ${cmd}" + eval "${cmd} 2>/dev/null" +} + +__kubectl_override_flag_list=(--kubeconfig --cluster --user --context --namespace --server -n -s) +__kubectl_override_flags() +{ + local ${__kubectl_override_flag_list[*]##*-} two_word_of of var + for w in "${words[@]}"; do + if [ -n "${two_word_of}" ]; then + eval "${two_word_of##*-}=\"${two_word_of}=\${w}\"" + two_word_of= + continue + fi + for of in "${__kubectl_override_flag_list[@]}"; do + case "${w}" in + ${of}=*) + eval "${of##*-}=\"${w}\"" + ;; + ${of}) + two_word_of="${of}" + ;; + esac + done + done + for var in "${__kubectl_override_flag_list[@]##*-}"; do + if eval "test -n \"\$${var}\""; then + eval "echo -n \${${var}}' '" + fi + done +} + +__kubectl_config_get_contexts() +{ + __kubectl_parse_config "contexts" +} + +__kubectl_config_get_clusters() +{ + __kubectl_parse_config "clusters" +} + +__kubectl_config_get_users() +{ + __kubectl_parse_config "users" +} + +# $1 has to be "contexts", "clusters" or "users" +__kubectl_parse_config() +{ + local template kubectl_out + template="{{ range .$1 }}{{ .name }} {{ end }}" + if kubectl_out=$(__kubectl_debug_out "kubectl config $(__kubectl_override_flags) -o template --template=\"${template}\" view"); then + COMPREPLY=( $( compgen -W "${kubectl_out[*]}" -- "$cur" ) ) + fi +} + +# $1 is the name of resource (required) +# $2 is template string for kubectl get (optional) +__kubectl_parse_get() +{ + local template + template="${2:-"{{ range .items }}{{ .metadata.name }} {{ end }}"}" + local kubectl_out + if kubectl_out=$(__kubectl_debug_out "kubectl get $(__kubectl_override_flags) -o template --template=\"${template}\" \"$1\""); then + COMPREPLY+=( $( compgen -W "${kubectl_out[*]}" -- "$cur" ) ) + fi +} + +__kubectl_get_resource() +{ + if [[ ${#nouns[@]} -eq 0 ]]; then + local kubectl_out + if kubectl_out=$(__kubectl_debug_out "kubectl api-resources $(__kubectl_override_flags) -o name --cached --request-timeout=5s --verbs=get"); then + COMPREPLY=( $( compgen -W "${kubectl_out[*]}" -- "$cur" ) ) + return 0 + fi + return 1 + fi + __kubectl_parse_get "${nouns[${#nouns[@]} -1]}" +} + +__kubectl_get_resource_namespace() +{ + __kubectl_parse_get "namespace" +} + +__kubectl_get_resource_pod() +{ + __kubectl_parse_get "pod" +} + +__kubectl_get_resource_rc() +{ + __kubectl_parse_get "rc" +} + +__kubectl_get_resource_node() +{ + __kubectl_parse_get "node" +} + +__kubectl_get_resource_clusterrole() +{ + __kubectl_parse_get "clusterrole" +} + +# $1 is the name of the pod we want to get the list of containers inside +__kubectl_get_containers() +{ + local template + template="{{ range .spec.initContainers }}{{ .name }} {{end}}{{ range .spec.containers }}{{ .name }} {{ end }}" + __kubectl_debug "${FUNCNAME} nouns are ${nouns[*]}" + + local len="${#nouns[@]}" + if [[ ${len} -ne 1 ]]; then + return + fi + local last=${nouns[${len} -1]} + local kubectl_out + if kubectl_out=$(__kubectl_debug_out "kubectl get $(__kubectl_override_flags) -o template --template=\"${template}\" pods \"${last}\""); then + COMPREPLY=( $( compgen -W "${kubectl_out[*]}" -- "$cur" ) ) + fi +} + +# Require both a pod and a container to be specified +__kubectl_require_pod_and_container() +{ + if [[ ${#nouns[@]} -eq 0 ]]; then + __kubectl_parse_get pods + return 0 + fi; + __kubectl_get_containers + return 0 +} + +__kubectl_cp() +{ + if [[ $(type -t compopt) = "builtin" ]]; then + compopt -o nospace + fi + + case "$cur" in + /*|[.~]*) # looks like a path + return + ;; + *:*) # TODO: complete remote files in the pod + return + ;; + */*) # complete / + local template namespace kubectl_out + template="{{ range .items }}{{ .metadata.namespace }}/{{ .metadata.name }}: {{ end }}" + namespace="${cur%%/*}" + if kubectl_out=$(__kubectl_debug_out "kubectl get $(__kubectl_override_flags) --namespace \"${namespace}\" -o template --template=\"${template}\" pods"); then + COMPREPLY=( $(compgen -W "${kubectl_out[*]}" -- "${cur}") ) + fi + return + ;; + *) # complete namespaces, pods, and filedirs + __kubectl_parse_get "namespace" "{{ range .items }}{{ .metadata.name }}/ {{ end }}" + __kubectl_parse_get "pod" "{{ range .items }}{{ .metadata.name }}: {{ end }}" + _filedir + ;; + esac +} + +__kubectl_custom_func() { + case ${last_command} in + kubectl_get | kubectl_describe | kubectl_delete | kubectl_label | kubectl_edit | kubectl_patch |\ + kubectl_annotate | kubectl_expose | kubectl_scale | kubectl_autoscale | kubectl_taint | kubectl_rollout_* |\ + kubectl_apply_edit-last-applied | kubectl_apply_view-last-applied) + __kubectl_get_resource + return + ;; + kubectl_logs) + __kubectl_require_pod_and_container + return + ;; + kubectl_exec | kubectl_port-forward | kubectl_top_pod | kubectl_attach) + __kubectl_get_resource_pod + return + ;; + kubectl_cordon | kubectl_uncordon | kubectl_drain | kubectl_top_node) + __kubectl_get_resource_node + return + ;; + kubectl_config_use-context | kubectl_config_rename-context | kubectl_config_delete-context) + __kubectl_config_get_contexts + return + ;; + kubectl_config_delete-cluster) + __kubectl_config_get_clusters + return + ;; + kubectl_cp) + __kubectl_cp + return + ;; + *) + ;; + esac +} +` +) + +var ( + bashCompletionFlags = map[string]string{ + "namespace": "__kubectl_get_resource_namespace", + "context": "__kubectl_config_get_contexts", + "cluster": "__kubectl_config_get_clusters", + "user": "__kubectl_config_get_users", + } +) + +// NewDefaultKubectlCommand creates the `kubectl` command with default arguments +func NewDefaultKubectlCommand() *cobra.Command { + return NewDefaultKubectlCommandWithArgs(NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes), os.Args, os.Stdin, os.Stdout, os.Stderr) +} + +// NewDefaultKubectlCommandWithArgs creates the `kubectl` command with arguments +func NewDefaultKubectlCommandWithArgs(pluginHandler PluginHandler, args []string, in io.Reader, out, errout io.Writer) *cobra.Command { + cmd := NewKubectlCommand(in, out, errout) + + if pluginHandler == nil { + return cmd + } + + if len(args) > 1 { + cmdPathPieces := args[1:] + + // only look for suitable extension executables if + // the specified command does not already exist + if _, _, err := cmd.Find(cmdPathPieces); err != nil { + if err := HandlePluginCommand(pluginHandler, cmdPathPieces); err != nil { + fmt.Fprintf(errout, "Error: %v\n", err) + os.Exit(1) + } + } + } + + return cmd +} + +// PluginHandler is capable of parsing command line arguments +// and performing executable filename lookups to search +// for valid plugin files, and execute found plugins. +type PluginHandler interface { + // exists at the given filename, or a boolean false. + // Lookup will iterate over a list of given prefixes + // in order to recognize valid plugin filenames. + // The first filepath to match a prefix is returned. + Lookup(filename string) (string, bool) + // Execute receives an executable's filepath, a slice + // of arguments, and a slice of environment variables + // to relay to the executable. + Execute(executablePath string, cmdArgs, environment []string) error +} + +// DefaultPluginHandler implements PluginHandler +type DefaultPluginHandler struct { + ValidPrefixes []string +} + +// NewDefaultPluginHandler instantiates the DefaultPluginHandler with a list of +// given filename prefixes used to identify valid plugin filenames. +func NewDefaultPluginHandler(validPrefixes []string) *DefaultPluginHandler { + return &DefaultPluginHandler{ + ValidPrefixes: validPrefixes, + } +} + +// Lookup implements PluginHandler +func (h *DefaultPluginHandler) Lookup(filename string) (string, bool) { + for _, prefix := range h.ValidPrefixes { + path, err := exec.LookPath(fmt.Sprintf("%s-%s", prefix, filename)) + if err != nil || len(path) == 0 { + continue + } + return path, true + } + + return "", false +} + +// Execute implements PluginHandler +func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error { + + // Windows does not support exec syscall. + if runtime.GOOS == "windows" { + cmd := exec.Command(executablePath, cmdArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + cmd.Env = environment + err := cmd.Run() + if err == nil { + os.Exit(0) + } + return err + } + + // invoke cmd binary relaying the environment and args given + // append executablePath to cmdArgs, as execve will make first argument the "binary name". + return syscall.Exec(executablePath, append([]string{executablePath}, cmdArgs...), environment) +} + +// HandlePluginCommand receives a pluginHandler and command-line arguments and attempts to find +// a plugin executable on the PATH that satisfies the given arguments. +func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string) error { + var remainingArgs []string // all "non-flag" arguments + for _, arg := range cmdArgs { + if strings.HasPrefix(arg, "-") { + break + } + remainingArgs = append(remainingArgs, strings.Replace(arg, "-", "_", -1)) + } + + if len(remainingArgs) == 0 { + // the length of cmdArgs is at least 1 + return fmt.Errorf("flags cannot be placed before plugin name: %s", cmdArgs[0]) + } + + foundBinaryPath := "" + + // attempt to find binary, starting at longest possible name with given cmdArgs + for len(remainingArgs) > 0 { + path, found := pluginHandler.Lookup(strings.Join(remainingArgs, "-")) + if !found { + remainingArgs = remainingArgs[:len(remainingArgs)-1] + continue + } + + foundBinaryPath = path + break + } + + if len(foundBinaryPath) == 0 { + return nil + } + + // invoke cmd binary relaying the current environment and args given + if err := pluginHandler.Execute(foundBinaryPath, cmdArgs[len(remainingArgs):], os.Environ()); err != nil { + return err + } + + return nil +} + +// NewKubectlCommand creates the `kubectl` command and its nested children. +func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command { + warningHandler := rest.NewWarningWriter(err, rest.WarningWriterOptions{Deduplicate: true, Color: term.AllowsColorOutput(err)}) + warningsAsErrors := false + + // Parent command to which all subcommands are added. + cmds := &cobra.Command{ + Use: "kubectl", + Short: i18n.T("kubectl controls the Kubernetes cluster manager"), + Long: templates.LongDesc(` + kubectl controls the Kubernetes cluster manager. + + Find more information at: + https://kubernetes.io/docs/reference/kubectl/overview/`), + Run: runHelp, + // Hook before and after Run initialize and write profiles to disk, + // respectively. + PersistentPreRunE: func(*cobra.Command, []string) error { + rest.SetDefaultWarningHandler(warningHandler) + return initProfiling() + }, + PersistentPostRunE: func(*cobra.Command, []string) error { + if err := flushProfiling(); err != nil { + return err + } + if warningsAsErrors { + count := warningHandler.WarningCount() + switch count { + case 0: + // no warnings + case 1: + return fmt.Errorf("%d warning received", count) + default: + return fmt.Errorf("%d warnings received", count) + } + } + return nil + }, + BashCompletionFunction: bashCompletionFunc, + } + + flags := cmds.PersistentFlags() + flags.SetNormalizeFunc(cliflag.WarnWordSepNormalizeFunc) // Warn for "_" flags + + // Normalize all flags that are coming from other packages or pre-configurations + // a.k.a. change all "_" to "-". e.g. glog package + flags.SetNormalizeFunc(cliflag.WordSepNormalizeFunc) + + addProfilingFlags(flags) + + flags.BoolVar(&warningsAsErrors, "warnings-as-errors", warningsAsErrors, "Treat warnings received from the server as errors and exit with a non-zero exit code") + + kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() + kubeConfigFlags.AddFlags(flags) + matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) + matchVersionKubeConfigFlags.AddFlags(cmds.PersistentFlags()) + + cmds.PersistentFlags().AddGoFlagSet(flag.CommandLine) + + f := cmdutil.NewFactory(matchVersionKubeConfigFlags) + + // Sending in 'nil' for the getLanguageFn() results in using + // the LANG environment variable. + // + // TODO: Consider adding a flag or file preference for setting + // the language, instead of just loading from the LANG env. variable. + i18n.LoadTranslations("kubectl", nil) + + // From this point and forward we get warnings on flags that contain "_" separators + cmds.SetGlobalNormalizationFunc(cliflag.WarnWordSepNormalizeFunc) + + ioStreams := genericclioptions.IOStreams{In: in, Out: out, ErrOut: err} + + groups := templates.CommandGroups{ + { + Message: "Basic Commands (Beginner):", + Commands: []*cobra.Command{ + create.NewCmdCreate(f, ioStreams), + expose.NewCmdExposeService(f, ioStreams), + run.NewCmdRun(f, ioStreams), + set.NewCmdSet(f, ioStreams), + }, + }, + { + Message: "Basic Commands (Intermediate):", + Commands: []*cobra.Command{ + explain.NewCmdExplain("kubectl", f, ioStreams), + get.NewCmdGet("kubectl", f, ioStreams), + edit.NewCmdEdit(f, ioStreams), + delete.NewCmdDelete(f, ioStreams), + }, + }, + { + Message: "Deploy Commands:", + Commands: []*cobra.Command{ + rollout.NewCmdRollout(f, ioStreams), + scale.NewCmdScale(f, ioStreams), + autoscale.NewCmdAutoscale(f, ioStreams), + }, + }, + { + Message: "Cluster Management Commands:", + Commands: []*cobra.Command{ + certificates.NewCmdCertificate(f, ioStreams), + clusterinfo.NewCmdClusterInfo(f, ioStreams), + top.NewCmdTop(f, ioStreams), + drain.NewCmdCordon(f, ioStreams), + drain.NewCmdUncordon(f, ioStreams), + drain.NewCmdDrain(f, ioStreams), + taint.NewCmdTaint(f, ioStreams), + }, + }, + { + Message: "Troubleshooting and Debugging Commands:", + Commands: []*cobra.Command{ + describe.NewCmdDescribe("kubectl", f, ioStreams), + logs.NewCmdLogs(f, ioStreams), + attach.NewCmdAttach(f, ioStreams), + cmdexec.NewCmdExec(f, ioStreams), + portforward.NewCmdPortForward(f, ioStreams), + proxy.NewCmdProxy(f, ioStreams), + cp.NewCmdCp(f, ioStreams), + auth.NewCmdAuth(f, ioStreams), + debug.NewCmdDebug(f, ioStreams, false), + }, + }, + { + Message: "Advanced Commands:", + Commands: []*cobra.Command{ + diff.NewCmdDiff(f, ioStreams), + apply.NewCmdApply("kubectl", f, ioStreams), + patch.NewCmdPatch(f, ioStreams), + replace.NewCmdReplace(f, ioStreams), + wait.NewCmdWait(f, ioStreams), + kustomize.NewCmdKustomize(ioStreams), + }, + }, + { + Message: "Settings Commands:", + Commands: []*cobra.Command{ + label.NewCmdLabel(f, ioStreams), + annotate.NewCmdAnnotate("kubectl", f, ioStreams), + completion.NewCmdCompletion(ioStreams.Out, ""), + }, + }, + } + groups.Add(cmds) + + filters := []string{"options"} + + // Hide the "alpha" subcommand if there are no alpha commands in this build. + alpha := NewCmdAlpha(f, ioStreams) + if !alpha.HasSubCommands() { + filters = append(filters, alpha.Name()) + } + + templates.ActsAsRootCommand(cmds, filters, groups...) + + for name, completion := range bashCompletionFlags { + if cmds.Flag(name) != nil { + if cmds.Flag(name).Annotations == nil { + cmds.Flag(name).Annotations = map[string][]string{} + } + cmds.Flag(name).Annotations[cobra.BashCompCustom] = append( + cmds.Flag(name).Annotations[cobra.BashCompCustom], + completion, + ) + } + } + + cmds.AddCommand(alpha) + cmds.AddCommand(cmdconfig.NewCmdConfig(f, clientcmd.NewDefaultPathOptions(), ioStreams)) + cmds.AddCommand(plugin.NewCmdPlugin(f, ioStreams)) + cmds.AddCommand(version.NewCmdVersion(f, ioStreams)) + cmds.AddCommand(apiresources.NewCmdAPIVersions(f, ioStreams)) + cmds.AddCommand(apiresources.NewCmdAPIResources(f, ioStreams)) + cmds.AddCommand(options.NewCmdOptions(ioStreams.Out)) + + return cmds +} + +func runHelp(cmd *cobra.Command, args []string) { + cmd.Help() +} diff --git a/pkg/cmd/cmd_test.go b/pkg/cmd/cmd_test.go new file mode 100644 index 000000000..f2a191797 --- /dev/null +++ b/pkg/cmd/cmd_test.go @@ -0,0 +1,160 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "io/ioutil" + "os" + "reflect" + "testing" + + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" +) + +func TestNormalizationFuncGlobalExistence(t *testing.T) { + // This test can be safely deleted when we will not support multiple flag formats + root := NewKubectlCommand(os.Stdin, os.Stdout, os.Stderr) + + if root.Parent() != nil { + t.Fatal("We expect the root command to be returned") + } + if root.GlobalNormalizationFunc() == nil { + t.Fatal("We expect that root command has a global normalization function") + } + + if reflect.ValueOf(root.GlobalNormalizationFunc()).Pointer() != reflect.ValueOf(root.Flags().GetNormalizeFunc()).Pointer() { + t.Fatal("root command seems to have a wrong normalization function") + } + + sub := root + for sub.HasSubCommands() { + sub = sub.Commands()[0] + } + + // In case of failure of this test check this PR: spf13/cobra#110 + if reflect.ValueOf(sub.Flags().GetNormalizeFunc()).Pointer() != reflect.ValueOf(root.Flags().GetNormalizeFunc()).Pointer() { + t.Fatal("child and root commands should have the same normalization functions") + } +} + +func TestKubectlCommandHandlesPlugins(t *testing.T) { + tests := []struct { + name string + args []string + expectPlugin string + expectPluginArgs []string + expectError string + }{ + { + name: "test that normal commands are able to be executed, when no plugin overshadows them", + args: []string{"kubectl", "get", "foo"}, + expectPlugin: "", + expectPluginArgs: []string{}, + }, + { + name: "test that a plugin executable is found based on command args", + args: []string{"kubectl", "foo", "--bar"}, + expectPlugin: "plugin/testdata/kubectl-foo", + expectPluginArgs: []string{"--bar"}, + }, + { + name: "test that a plugin does not execute over an existing command by the same name", + args: []string{"kubectl", "version"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pluginsHandler := &testPluginHandler{ + pluginsDirectory: "plugin/testdata", + } + _, in, out, errOut := genericclioptions.NewTestIOStreams() + + cmdutil.BehaviorOnFatal(func(str string, code int) { + errOut.Write([]byte(str)) + }) + + root := NewDefaultKubectlCommandWithArgs(pluginsHandler, test.args, in, out, errOut) + if err := root.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if pluginsHandler.err != nil && pluginsHandler.err.Error() != test.expectError { + t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectError, pluginsHandler.err) + } + + if pluginsHandler.executedPlugin != test.expectPlugin { + t.Fatalf("unexpected plugin execution: expected %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin) + } + + if len(pluginsHandler.withArgs) != len(test.expectPluginArgs) { + t.Fatalf("unexpected plugin execution args: expected %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs) + } + }) + } +} + +type testPluginHandler struct { + pluginsDirectory string + + // execution results + executedPlugin string + withArgs []string + withEnv []string + + err error +} + +func (h *testPluginHandler) Lookup(filename string) (string, bool) { + // append supported plugin prefix to the filename + filename = fmt.Sprintf("%s-%s", "kubectl", filename) + + dir, err := os.Stat(h.pluginsDirectory) + if err != nil { + h.err = err + return "", false + } + + if !dir.IsDir() { + h.err = fmt.Errorf("expected %q to be a directory", h.pluginsDirectory) + return "", false + } + + plugins, err := ioutil.ReadDir(h.pluginsDirectory) + if err != nil { + h.err = err + return "", false + } + + for _, p := range plugins { + if p.Name() == filename { + return fmt.Sprintf("%s/%s", h.pluginsDirectory, p.Name()), true + } + } + + h.err = fmt.Errorf("unable to find a plugin executable %q", filename) + return "", false +} + +func (h *testPluginHandler) Execute(executablePath string, cmdArgs, env []string) error { + h.executedPlugin = executablePath + h.withArgs = cmdArgs + h.withEnv = env + return nil +} diff --git a/pkg/cmd/profiling.go b/pkg/cmd/profiling.go new file mode 100644 index 000000000..bfe2a16eb --- /dev/null +++ b/pkg/cmd/profiling.go @@ -0,0 +1,100 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "os" + "os/signal" + "runtime" + "runtime/pprof" + + "github.com/spf13/pflag" +) + +var ( + profileName string + profileOutput string +) + +func addProfilingFlags(flags *pflag.FlagSet) { + flags.StringVar(&profileName, "profile", "none", "Name of profile to capture. One of (none|cpu|heap|goroutine|threadcreate|block|mutex)") + flags.StringVar(&profileOutput, "profile-output", "profile.pprof", "Name of the file to write the profile to") +} + +func initProfiling() error { + switch profileName { + case "none": + return nil + case "cpu": + f, err := os.Create(profileOutput) + if err != nil { + return err + } + err = pprof.StartCPUProfile(f) + if err != nil { + return err + } + // Block and mutex profiles need a call to Set{Block,Mutex}ProfileRate to + // output anything. We choose to sample all events. + case "block": + runtime.SetBlockProfileRate(1) + case "mutex": + runtime.SetMutexProfileFraction(1) + default: + // Check the profile name is valid. + if profile := pprof.Lookup(profileName); profile == nil { + return fmt.Errorf("unknown profile '%s'", profileName) + } + } + + // If the command is interrupted before the end (ctrl-c), flush the + // profiling files + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + <-c + flushProfiling() + os.Exit(0) + }() + + return nil +} + +func flushProfiling() error { + switch profileName { + case "none": + return nil + case "cpu": + pprof.StopCPUProfile() + case "heap": + runtime.GC() + fallthrough + default: + profile := pprof.Lookup(profileName) + if profile == nil { + return nil + } + f, err := os.Create(profileOutput) + if err != nil { + return err + } + profile.WriteTo(f, 0) + } + + return nil +}