1
16
17 package plugin
18
19 import (
20 "bytes"
21 "fmt"
22 "os"
23 "path/filepath"
24 "runtime"
25 "strings"
26
27 "github.com/spf13/cobra"
28
29 "k8s.io/cli-runtime/pkg/genericiooptions"
30
31 cmdutil "k8s.io/kubectl/pkg/cmd/util"
32 "k8s.io/kubectl/pkg/util/i18n"
33 "k8s.io/kubectl/pkg/util/templates"
34 )
35
36 var (
37 pluginLong = templates.LongDesc(i18n.T(`
38 Provides utilities for interacting with plugins.
39
40 Plugins provide extended functionality that is not part of the major command-line distribution.
41 Please refer to the documentation and examples for more information about how write your own plugins.
42
43 The easiest way to discover and install plugins is via the kubernetes sub-project krew.
44 To install krew, visit [krew.sigs.k8s.io](https://krew.sigs.k8s.io/docs/user-guide/setup/install/)`))
45
46 pluginExample = templates.Examples(i18n.T(`
47 # List all available plugins
48 kubectl plugin list`))
49
50 pluginListLong = templates.LongDesc(i18n.T(`
51 List all available plugin files on a user's PATH.
52
53 Available plugin files are those that are:
54 - executable
55 - anywhere on the user's PATH
56 - begin with "kubectl-"
57 `))
58
59 ValidPluginFilenamePrefixes = []string{"kubectl"}
60 )
61
62 func NewCmdPlugin(streams genericiooptions.IOStreams) *cobra.Command {
63 cmd := &cobra.Command{
64 Use: "plugin [flags]",
65 DisableFlagsInUseLine: true,
66 Short: i18n.T("Provides utilities for interacting with plugins"),
67 Long: pluginLong,
68 Run: func(cmd *cobra.Command, args []string) {
69 cmdutil.DefaultSubCommandRun(streams.ErrOut)(cmd, args)
70 },
71 }
72
73 cmd.AddCommand(NewCmdPluginList(streams))
74 return cmd
75 }
76
77 type PluginListOptions struct {
78 Verifier PathVerifier
79 NameOnly bool
80
81 PluginPaths []string
82
83 genericiooptions.IOStreams
84 }
85
86
87 func NewCmdPluginList(streams genericiooptions.IOStreams) *cobra.Command {
88 o := &PluginListOptions{
89 IOStreams: streams,
90 }
91
92 cmd := &cobra.Command{
93 Use: "list",
94 Short: i18n.T("List all visible plugin executables on a user's PATH"),
95 Example: pluginExample,
96 Long: pluginListLong,
97 Run: func(cmd *cobra.Command, args []string) {
98 cmdutil.CheckErr(o.Complete(cmd))
99 cmdutil.CheckErr(o.Run())
100 },
101 }
102
103 cmd.Flags().BoolVar(&o.NameOnly, "name-only", o.NameOnly, "If true, display only the binary name of each plugin, rather than its full path")
104 return cmd
105 }
106
107 func (o *PluginListOptions) Complete(cmd *cobra.Command) error {
108 o.Verifier = &CommandOverrideVerifier{
109 root: cmd.Root(),
110 seenPlugins: make(map[string]string),
111 }
112
113 o.PluginPaths = filepath.SplitList(os.Getenv("PATH"))
114 return nil
115 }
116
117 func (o *PluginListOptions) Run() error {
118 plugins, pluginErrors := o.ListPlugins()
119
120 if len(plugins) > 0 {
121 fmt.Fprintf(o.Out, "The following compatible plugins are available:\n\n")
122 } else {
123 pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to find any kubectl plugins in your PATH"))
124 }
125
126 pluginWarnings := 0
127 for _, pluginPath := range plugins {
128 if o.NameOnly {
129 fmt.Fprintf(o.Out, "%s\n", filepath.Base(pluginPath))
130 } else {
131 fmt.Fprintf(o.Out, "%s\n", pluginPath)
132 }
133 if errs := o.Verifier.Verify(pluginPath); len(errs) != 0 {
134 for _, err := range errs {
135 fmt.Fprintf(o.ErrOut, " - %s\n", err)
136 pluginWarnings++
137 }
138 }
139 }
140
141 if pluginWarnings > 0 {
142 if pluginWarnings == 1 {
143 pluginErrors = append(pluginErrors, fmt.Errorf("error: one plugin warning was found"))
144 } else {
145 pluginErrors = append(pluginErrors, fmt.Errorf("error: %v plugin warnings were found", pluginWarnings))
146 }
147 }
148 if len(pluginErrors) > 0 {
149 errs := bytes.NewBuffer(nil)
150 for _, e := range pluginErrors {
151 fmt.Fprintln(errs, e)
152 }
153 return fmt.Errorf("%s", errs.String())
154 }
155
156 return nil
157 }
158
159
160 func (o *PluginListOptions) ListPlugins() ([]string, []error) {
161 plugins := []string{}
162 errors := []error{}
163
164 for _, dir := range uniquePathsList(o.PluginPaths) {
165 if len(strings.TrimSpace(dir)) == 0 {
166 continue
167 }
168
169 files, err := os.ReadDir(dir)
170 if err != nil {
171 if _, ok := err.(*os.PathError); ok {
172 fmt.Fprintf(o.ErrOut, "Unable to read directory %q from your PATH: %v. Skipping...\n", dir, err)
173 continue
174 }
175
176 errors = append(errors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err))
177 continue
178 }
179
180 for _, f := range files {
181 if f.IsDir() {
182 continue
183 }
184 if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) {
185 continue
186 }
187
188 plugins = append(plugins, filepath.Join(dir, f.Name()))
189 }
190 }
191
192 return plugins, errors
193 }
194
195
196 type PathVerifier interface {
197
198 Verify(path string) []error
199 }
200
201 type CommandOverrideVerifier struct {
202 root *cobra.Command
203 seenPlugins map[string]string
204 }
205
206
207
208
209 func (v *CommandOverrideVerifier) Verify(path string) []error {
210 if v.root == nil {
211 return []error{fmt.Errorf("unable to verify path with nil root")}
212 }
213
214
215 segs := strings.Split(path, "/")
216 binName := segs[len(segs)-1]
217
218 cmdPath := strings.Split(binName, "-")
219 if len(cmdPath) > 1 {
220
221 cmdPath = cmdPath[1:]
222 }
223
224 errors := []error{}
225
226 if isExec, err := isExecutable(path); err == nil && !isExec {
227 errors = append(errors, fmt.Errorf("warning: %s identified as a kubectl plugin, but it is not executable", path))
228 } else if err != nil {
229 errors = append(errors, fmt.Errorf("error: unable to identify %s as an executable file: %v", path, err))
230 }
231
232 if existingPath, ok := v.seenPlugins[binName]; ok {
233 errors = append(errors, fmt.Errorf("warning: %s is overshadowed by a similarly named plugin: %s", path, existingPath))
234 } else {
235 v.seenPlugins[binName] = path
236 }
237
238 if cmd, _, err := v.root.Find(cmdPath); err == nil {
239 errors = append(errors, fmt.Errorf("warning: %s overwrites existing command: %q", binName, cmd.CommandPath()))
240 }
241
242 return errors
243 }
244
245 func isExecutable(fullPath string) (bool, error) {
246 info, err := os.Stat(fullPath)
247 if err != nil {
248 return false, err
249 }
250
251 if runtime.GOOS == "windows" {
252 fileExt := strings.ToLower(filepath.Ext(fullPath))
253
254 switch fileExt {
255 case ".bat", ".cmd", ".com", ".exe", ".ps1":
256 return true, nil
257 }
258 return false, nil
259 }
260
261 if m := info.Mode(); !m.IsDir() && m&0111 != 0 {
262 return true, nil
263 }
264
265 return false, nil
266 }
267
268
269
270 func uniquePathsList(paths []string) []string {
271 seen := map[string]bool{}
272 newPaths := []string{}
273 for _, p := range paths {
274 if seen[p] {
275 continue
276 }
277 seen[p] = true
278 newPaths = append(newPaths, p)
279 }
280 return newPaths
281 }
282
283 func hasValidPrefix(filepath string, validPrefixes []string) bool {
284 for _, prefix := range validPrefixes {
285 if !strings.HasPrefix(filepath, prefix+"-") {
286 continue
287 }
288 return true
289 }
290 return false
291 }
292
View as plain text