github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/plugin/plugin.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package plugin 21 22 import ( 23 "bytes" 24 "fmt" 25 "io" 26 "os" 27 "path/filepath" 28 "strings" 29 30 "github.com/spf13/cobra" 31 "k8s.io/cli-runtime/pkg/genericiooptions" 32 "k8s.io/klog/v2" 33 cmdutil "k8s.io/kubectl/pkg/cmd/util" 34 "k8s.io/kubectl/pkg/util/templates" 35 36 "github.com/1aal/kubeblocks/pkg/cli/printer" 37 "github.com/1aal/kubeblocks/pkg/cli/util" 38 ) 39 40 var ( 41 pluginLong = templates.LongDesc(` 42 Provides utilities for interacting with plugins. 43 44 Plugins provide extended functionality that is not part of the major command-line distribution. 45 `) 46 47 pluginListExample = templates.Examples(` 48 # List all available plugins file on a user's PATH. 49 kbcli plugin list 50 `) 51 52 ValidPluginFilenamePrefixes = []string{"kbcli", "kubectl"} 53 paths = GetKbcliPluginPath() 54 ) 55 56 func NewPluginCmd(streams genericiooptions.IOStreams) *cobra.Command { 57 cmd := &cobra.Command{ 58 Use: "plugin", 59 Short: "Provides utilities for interacting with plugins.", 60 Long: pluginLong, 61 PersistentPreRun: func(cmd *cobra.Command, args []string) { 62 InitPlugin() 63 }, 64 } 65 66 cmd.AddCommand( 67 NewPluginListCmd(streams), 68 NewPluginIndexCmd(streams), 69 NewPluginInstallCmd(streams), 70 NewPluginUninstallCmd(streams), 71 NewPluginSearchCmd(streams), 72 NewPluginDescribeCmd(streams), 73 NewPluginUpgradeCmd(streams), 74 ) 75 return cmd 76 } 77 78 type PluginListOptions struct { 79 Verifier PathVerifier 80 81 PluginPaths []string 82 83 genericiooptions.IOStreams 84 } 85 86 func NewPluginListCmd(streams genericiooptions.IOStreams) *cobra.Command { 87 o := &PluginListOptions{ 88 IOStreams: streams, 89 } 90 cmd := &cobra.Command{ 91 Use: "list", 92 DisableFlagsInUseLine: true, 93 Short: "List all visible plugin executables on a user's PATH", 94 Example: pluginListExample, 95 Run: func(cmd *cobra.Command, args []string) { 96 cmdutil.CheckErr(o.Complete(cmd)) 97 cmdutil.CheckErr(o.Run()) 98 }, 99 } 100 return cmd 101 } 102 103 func (o *PluginListOptions) Complete(cmd *cobra.Command) error { 104 o.Verifier = &CommandOverrideVerifier{ 105 root: cmd.Root(), 106 seenPlugins: map[string]string{}, 107 } 108 109 o.PluginPaths = filepath.SplitList(os.Getenv("PATH")) 110 return nil 111 } 112 113 func (o *PluginListOptions) Run() error { 114 plugins, pluginErrors := o.ListPlugins() 115 116 if len(plugins) == 0 { 117 pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to find any kbcli or kubectl plugins in your PATH")) 118 } 119 120 pluginWarnings := 0 121 p := NewPluginPrinter(o.IOStreams.Out) 122 errMsg := "" 123 for _, pluginPath := range plugins { 124 name := filepath.Base(pluginPath) 125 path := filepath.Dir(pluginPath) 126 if errs := o.Verifier.Verify(pluginPath); len(errs) != 0 { 127 for _, err := range errs { 128 errMsg += fmt.Sprintf("%s\n", err) 129 pluginWarnings++ 130 } 131 } 132 addPluginRow(name, path, p) 133 } 134 p.Print() 135 klog.V(1).Info(errMsg) 136 137 if pluginWarnings > 0 { 138 if pluginWarnings == 1 { 139 pluginErrors = append(pluginErrors, fmt.Errorf("error: one plugin warining was found")) 140 } else { 141 pluginErrors = append(pluginErrors, fmt.Errorf("error: %d plugin warnings were found", pluginWarnings)) 142 } 143 } 144 if len(pluginErrors) > 0 { 145 errs := bytes.NewBuffer(nil) 146 for _, e := range pluginErrors { 147 fmt.Fprintln(errs, e) 148 } 149 return fmt.Errorf("%s", errs.String()) 150 } 151 152 return nil 153 } 154 155 func (o *PluginListOptions) ListPlugins() ([]string, []error) { 156 var plugins []string 157 var errors []error 158 159 for _, dir := range uniquePathsList(o.PluginPaths) { 160 if len(strings.TrimSpace(dir)) == 0 { 161 continue 162 } 163 164 files, err := os.ReadDir(dir) 165 if err != nil { 166 if _, ok := err.(*os.PathError); ok { 167 klog.V(1).Info("Unable to read directory %q from your PATH: %v. Skipping...\n", dir, err) 168 continue 169 } 170 171 errors = append(errors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err)) 172 continue 173 } 174 175 for _, f := range files { 176 if f.IsDir() { 177 continue 178 } 179 if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) { 180 continue 181 } 182 183 plugins = append(plugins, filepath.Join(dir, f.Name())) 184 } 185 } 186 187 return plugins, errors 188 } 189 190 // PathVerifier receives a path and validates it. 191 type PathVerifier interface { 192 Verify(path string) []error 193 } 194 195 type CommandOverrideVerifier struct { 196 root *cobra.Command 197 seenPlugins map[string]string 198 } 199 200 // Verify implements PathVerifier and determines if a given path 201 // is valid depending on whether it overwrites an existing 202 // kbcli command path, or a previously seen plugin. 203 func (v *CommandOverrideVerifier) Verify(path string) []error { 204 if v.root == nil { 205 return []error{fmt.Errorf("unable to verify path with nil root")} 206 } 207 208 // extract the plugin binary name 209 binName := filepath.Base(path) 210 211 cmdPath := strings.Split(binName, "-") 212 if len(cmdPath) > 1 { 213 // the first argument is always "kbcli" or "kubectl" for a plugin binary 214 cmdPath = cmdPath[1:] 215 } 216 217 var errors []error 218 if isExec, err := isExecutable(path); err == nil && !isExec { 219 errors = append(errors, fmt.Errorf("warning: %q identified as a kbcli or kubectl plugin, but it is not executable", path)) 220 } else if err != nil { 221 errors = append(errors, fmt.Errorf("error: unable to identify %s as an executable file: %v", path, err)) 222 } 223 224 if existingPath, ok := v.seenPlugins[binName]; ok { 225 errors = append(errors, fmt.Errorf("warning: %s is overshadowed by a similarly named plugin: %s", path, existingPath)) 226 } else { 227 v.seenPlugins[binName] = path 228 } 229 230 if cmd, _, err := v.root.Find(cmdPath); err == nil { 231 errors = append(errors, fmt.Errorf("warning: %q overwrites existing kbcli command: %q", path, cmd.CommandPath())) 232 } 233 234 return errors 235 } 236 237 func isExecutable(fullPath string) (bool, error) { 238 info, err := os.Stat(fullPath) 239 if err != nil { 240 return false, err 241 } 242 243 if util.IsWindows() { 244 fileExt := strings.ToLower(filepath.Ext(fullPath)) 245 246 switch fileExt { 247 case ".bat", ".cmd", ".com", ".exe", ".ps1": 248 return true, nil 249 } 250 return false, nil 251 } 252 253 if m := info.Mode(); !m.IsDir() && m&0111 != 0 { 254 return true, nil 255 } 256 257 return false, nil 258 } 259 260 func uniquePathsList(paths []string) []string { 261 var newPaths []string 262 seen := map[string]bool{} 263 264 for _, path := range paths { 265 if !seen[path] { 266 newPaths = append(newPaths, path) 267 seen[path] = true 268 } 269 } 270 return newPaths 271 } 272 273 func hasValidPrefix(filepath string, validPrefixes []string) bool { 274 for _, prefix := range validPrefixes { 275 if strings.HasPrefix(filepath, prefix+"-") { 276 return true 277 } 278 } 279 return false 280 } 281 282 func NewPluginPrinter(out io.Writer) *printer.TablePrinter { 283 t := printer.NewTablePrinter(out) 284 t.SetHeader("NAME", "PATH") 285 return t 286 } 287 288 func addPluginRow(name, path string, p *printer.TablePrinter) { 289 p.AddRow(name, path) 290 } 291 292 func InitPlugin() { 293 // Ensure that the base directories exist 294 if err := EnsureDirs(paths.BasePath(), 295 paths.BinPath(), 296 paths.InstallPath(), 297 paths.IndexBase(), 298 paths.InstallReceiptsPath()); err != nil { 299 klog.Fatal(err) 300 } 301 302 // check if index exists, if indexes don't exist, download default index 303 indexes, err := ListIndexes(paths) 304 if err != nil { 305 klog.Fatal(err) 306 } 307 if len(indexes) == 0 { 308 klog.V(1).Info("no index found, downloading default index") 309 if err := AddIndex(paths, DefaultIndexName, DefaultIndexURI); err != nil { 310 klog.Fatal("failed to download default index", err) 311 } 312 if err := AddIndex(paths, KrewIndexName, KrewIndexURI); err != nil { 313 klog.Fatal("failed to download krew index", err) 314 } 315 } 316 }