github.com/codefresh-io/kcfi@v0.0.0-20230301195427-c1578715cc46/cmd/kcfi/load_plugins.go (about) 1 /* 2 Copyright The Helm Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package main 17 18 import ( 19 "bytes" 20 "fmt" 21 "io" 22 "io/ioutil" 23 "log" 24 "os" 25 "os/exec" 26 "path/filepath" 27 "strconv" 28 "strings" 29 "syscall" 30 31 "github.com/pkg/errors" 32 "github.com/spf13/cobra" 33 "sigs.k8s.io/yaml" 34 35 "github.com/codefresh-io/kcfi/pkg/helm-internal/completion" 36 "helm.sh/helm/v3/pkg/plugin" 37 ) 38 39 const ( 40 pluginStaticCompletionFile = "completion.yaml" 41 pluginDynamicCompletionExecutable = "plugin.complete" 42 ) 43 44 type pluginError struct { 45 error 46 code int 47 } 48 49 // loadPlugins loads plugins into the command list. 50 // 51 // This follows a different pattern than the other commands because it has 52 // to inspect its environment and then add commands to the base command 53 // as it finds them. 54 func loadPlugins(baseCmd *cobra.Command, out io.Writer) { 55 56 // If HELM_NO_PLUGINS is set to 1, do not load plugins. 57 if os.Getenv("HELM_NO_PLUGINS") == "1" { 58 return 59 } 60 61 found, err := findPlugins(settings.PluginsDirectory) 62 if err != nil { 63 fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err) 64 return 65 } 66 67 processParent := func(cmd *cobra.Command, args []string) ([]string, error) { 68 k, u := manuallyProcessArgs(args) 69 if err := cmd.Parent().ParseFlags(k); err != nil { 70 return nil, err 71 } 72 return u, nil 73 } 74 75 // If we are dealing with the completion command, we try to load more details about the plugins 76 // if available, so as to allow for command and flag completion 77 if subCmd, _, err := baseCmd.Find(os.Args[1:]); err == nil && subCmd.Name() == "completion" { 78 loadPluginsForCompletion(baseCmd, found) 79 return 80 } 81 82 // Now we create commands for all of these. 83 for _, plug := range found { 84 plug := plug 85 md := plug.Metadata 86 if md.Usage == "" { 87 md.Usage = fmt.Sprintf("the %q plugin", md.Name) 88 } 89 90 // This function is used to setup the environment for the plugin and then 91 // call the executable specified by the parameter 'main' 92 callPluginExecutable := func(cmd *cobra.Command, main string, argv []string, out io.Writer) error { 93 env := os.Environ() 94 for k, v := range settings.EnvVars() { 95 env = append(env, fmt.Sprintf("%s=%s", k, v)) 96 } 97 98 prog := exec.Command(main, argv...) 99 prog.Env = env 100 prog.Stdin = os.Stdin 101 prog.Stdout = out 102 prog.Stderr = os.Stderr 103 if err := prog.Run(); err != nil { 104 if eerr, ok := err.(*exec.ExitError); ok { 105 os.Stderr.Write(eerr.Stderr) 106 status := eerr.Sys().(syscall.WaitStatus) 107 return pluginError{ 108 error: errors.Errorf("plugin %q exited with error", md.Name), 109 code: status.ExitStatus(), 110 } 111 } 112 return err 113 } 114 return nil 115 } 116 117 c := &cobra.Command{ 118 Use: md.Name, 119 Short: md.Usage, 120 Long: md.Description, 121 RunE: func(cmd *cobra.Command, args []string) error { 122 u, err := processParent(cmd, args) 123 if err != nil { 124 return err 125 } 126 127 // Call setupEnv before PrepareCommand because 128 // PrepareCommand uses os.ExpandEnv and expects the 129 // setupEnv vars. 130 plugin.SetupPluginEnv(settings, md.Name, plug.Dir) 131 main, argv, prepCmdErr := plug.PrepareCommand(u) 132 if prepCmdErr != nil { 133 os.Stderr.WriteString(prepCmdErr.Error()) 134 return errors.Errorf("plugin %q exited with error", md.Name) 135 } 136 137 return callPluginExecutable(cmd, main, argv, out) 138 }, 139 // This passes all the flags to the subcommand. 140 DisableFlagParsing: true, 141 } 142 143 // Setup dynamic completion for the plugin 144 completion.RegisterValidArgsFunc(c, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) { 145 u, err := processParent(cmd, args) 146 if err != nil { 147 return nil, completion.BashCompDirectiveError 148 } 149 150 // We will call the dynamic completion script of the plugin 151 main := strings.Join([]string{plug.Dir, pluginDynamicCompletionExecutable}, string(filepath.Separator)) 152 153 argv := []string{} 154 if !md.IgnoreFlags { 155 argv = append(argv, u...) 156 argv = append(argv, toComplete) 157 } 158 plugin.SetupPluginEnv(settings, md.Name, plug.Dir) 159 160 completion.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv)) 161 buf := new(bytes.Buffer) 162 if err := callPluginExecutable(cmd, main, argv, buf); err != nil { 163 return nil, completion.BashCompDirectiveError 164 } 165 166 var completions []string 167 for _, comp := range strings.Split(buf.String(), "\n") { 168 // Remove any empty lines 169 if len(comp) > 0 { 170 completions = append(completions, comp) 171 } 172 } 173 174 // Check if the last line of output is of the form :<integer>, which 175 // indicates the BashCompletionDirective. 176 directive := completion.BashCompDirectiveDefault 177 if len(completions) > 0 { 178 lastLine := completions[len(completions)-1] 179 if len(lastLine) > 1 && lastLine[0] == ':' { 180 if strInt, err := strconv.Atoi(lastLine[1:]); err == nil { 181 directive = completion.BashCompDirective(strInt) 182 completions = completions[:len(completions)-1] 183 } 184 } 185 } 186 187 return completions, directive 188 }) 189 190 // TODO: Make sure a command with this name does not already exist. 191 baseCmd.AddCommand(c) 192 } 193 } 194 195 // manuallyProcessArgs processes an arg array, removing special args. 196 // 197 // Returns two sets of args: known and unknown (in that order) 198 func manuallyProcessArgs(args []string) ([]string, []string) { 199 known := []string{} 200 unknown := []string{} 201 kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--registry-config", "--repository-cache", "--repository-config"} 202 knownArg := func(a string) bool { 203 for _, pre := range kvargs { 204 if strings.HasPrefix(a, pre+"=") { 205 return true 206 } 207 } 208 return false 209 } 210 211 isKnown := func(v string) string { 212 for _, i := range kvargs { 213 if i == v { 214 return v 215 } 216 } 217 return "" 218 } 219 220 for i := 0; i < len(args); i++ { 221 switch a := args[i]; a { 222 case "--debug": 223 known = append(known, a) 224 case isKnown(a): 225 known = append(known, a) 226 i++ 227 if i < len(args) { 228 known = append(known, args[i]) 229 } 230 default: 231 if knownArg(a) { 232 known = append(known, a) 233 continue 234 } 235 unknown = append(unknown, a) 236 } 237 } 238 return known, unknown 239 } 240 241 // findPlugins returns a list of YAML files that describe plugins. 242 func findPlugins(plugdirs string) ([]*plugin.Plugin, error) { 243 found := []*plugin.Plugin{} 244 // Let's get all UNIXy and allow path separators 245 for _, p := range filepath.SplitList(plugdirs) { 246 matches, err := plugin.LoadAll(p) 247 if err != nil { 248 return matches, err 249 } 250 found = append(found, matches...) 251 } 252 return found, nil 253 } 254 255 // pluginCommand represents the optional completion.yaml file of a plugin 256 type pluginCommand struct { 257 Name string `json:"name"` 258 ValidArgs []string `json:"validArgs"` 259 Flags []string `json:"flags"` 260 Commands []pluginCommand `json:"commands"` 261 } 262 263 // loadPluginsForCompletion will load and parse any completion.yaml provided by the plugins 264 func loadPluginsForCompletion(baseCmd *cobra.Command, plugins []*plugin.Plugin) { 265 for _, plug := range plugins { 266 // Parse the yaml file providing the plugin's subcmds and flags 267 cmds, err := loadFile(strings.Join( 268 []string{plug.Dir, pluginStaticCompletionFile}, string(filepath.Separator))) 269 270 if err != nil { 271 // The file could be missing or invalid. Either way, we at least create the command 272 // for the plugin name. 273 if settings.Debug { 274 log.Output(2, fmt.Sprintf("[info] %s\n", err.Error())) 275 } 276 cmds = &pluginCommand{Name: plug.Metadata.Name} 277 } 278 279 // We know what the plugin name must be. 280 // Let's set it in case the Name field was not specified correctly in the file. 281 // This insures that we will at least get the plugin name to complete, even if 282 // there is a problem with the completion.yaml file 283 cmds.Name = plug.Metadata.Name 284 285 addPluginCommands(baseCmd, cmds) 286 } 287 } 288 289 // addPluginCommands is a recursive method that adds the different levels 290 // of sub-commands and flags for the plugins that provide such information 291 func addPluginCommands(baseCmd *cobra.Command, cmds *pluginCommand) { 292 if cmds == nil { 293 return 294 } 295 296 if len(cmds.Name) == 0 { 297 // Missing name for a command 298 if settings.Debug { 299 log.Output(2, fmt.Sprintf("[info] sub-command name field missing for %s", baseCmd.CommandPath())) 300 } 301 return 302 } 303 304 // Create a fake command just so the completion script will include it 305 c := &cobra.Command{ 306 Use: cmds.Name, 307 ValidArgs: cmds.ValidArgs, 308 // A Run is required for it to be a valid command without subcommands 309 Run: func(cmd *cobra.Command, args []string) {}, 310 } 311 baseCmd.AddCommand(c) 312 313 // Create fake flags. 314 if len(cmds.Flags) > 0 { 315 // The flags can be created with any type, since we only need them for completion. 316 // pflag does not allow to create short flags without a corresponding long form 317 // so we look for all short flags and match them to any long flag. This will allow 318 // plugins to provide short flags without a long form. 319 // If there are more short-flags than long ones, we'll create an extra long flag with 320 // the same single letter as the short form. 321 shorts := []string{} 322 longs := []string{} 323 for _, flag := range cmds.Flags { 324 if len(flag) == 1 { 325 shorts = append(shorts, flag) 326 } else { 327 longs = append(longs, flag) 328 } 329 } 330 331 f := c.Flags() 332 if len(longs) >= len(shorts) { 333 for i := range longs { 334 if i < len(shorts) { 335 f.BoolP(longs[i], shorts[i], false, "") 336 } else { 337 f.Bool(longs[i], false, "") 338 } 339 } 340 } else { 341 for i := range shorts { 342 if i < len(longs) { 343 f.BoolP(longs[i], shorts[i], false, "") 344 } else { 345 // Create a long flag with the same name as the short flag. 346 // Not a perfect solution, but its better than ignoring the extra short flags. 347 f.BoolP(shorts[i], shorts[i], false, "") 348 } 349 } 350 } 351 } 352 353 // Recursively add any sub-commands 354 for _, cmd := range cmds.Commands { 355 addPluginCommands(c, &cmd) 356 } 357 } 358 359 // loadFile takes a yaml file at the given path, parses it and returns a pluginCommand object 360 func loadFile(path string) (*pluginCommand, error) { 361 cmds := new(pluginCommand) 362 b, err := ioutil.ReadFile(path) 363 if err != nil { 364 return cmds, errors.New(fmt.Sprintf("File (%s) not provided by plugin. No plugin auto-completion possible.", path)) 365 } 366 367 err = yaml.Unmarshal(b, cmds) 368 return cmds, err 369 }