sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/cli/options.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package cli 18 19 import ( 20 "errors" 21 "fmt" 22 "io/fs" 23 "os" 24 "path/filepath" 25 "runtime" 26 "strings" 27 28 "github.com/sirupsen/logrus" 29 "github.com/spf13/afero" 30 "github.com/spf13/cobra" 31 32 "sigs.k8s.io/kubebuilder/v3/pkg/config" 33 cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" 34 cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" 35 "sigs.k8s.io/kubebuilder/v3/pkg/plugin" 36 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/external" 37 ) 38 39 var retrievePluginsRoot = getPluginsRoot 40 41 // Option is a function used as arguments to New in order to configure the resulting CLI. 42 type Option func(*CLI) error 43 44 // WithCommandName is an Option that sets the CLI's root command name. 45 func WithCommandName(name string) Option { 46 return func(c *CLI) error { 47 c.commandName = name 48 return nil 49 } 50 } 51 52 // WithVersion is an Option that defines the version string of the CLI. 53 func WithVersion(version string) Option { 54 return func(c *CLI) error { 55 c.version = version 56 return nil 57 } 58 } 59 60 // WithDescription is an Option that sets the CLI's root description. 61 func WithDescription(description string) Option { 62 return func(c *CLI) error { 63 c.description = description 64 return nil 65 } 66 } 67 68 // WithPlugins is an Option that sets the CLI's plugins. 69 // 70 // Specifying any invalid plugin results in an error. 71 func WithPlugins(plugins ...plugin.Plugin) Option { 72 return func(c *CLI) error { 73 for _, p := range plugins { 74 key := plugin.KeyFor(p) 75 if _, isConflicting := c.plugins[key]; isConflicting { 76 return fmt.Errorf("two plugins have the same key: %q", key) 77 } 78 if err := plugin.Validate(p); err != nil { 79 return fmt.Errorf("broken pre-set plugin %q: %v", key, err) 80 } 81 c.plugins[key] = p 82 } 83 return nil 84 } 85 } 86 87 // WithDefaultPlugins is an Option that sets the CLI's default plugins. 88 // 89 // Specifying any invalid plugin results in an error. 90 func WithDefaultPlugins(projectVersion config.Version, plugins ...plugin.Plugin) Option { 91 return func(c *CLI) error { 92 if err := projectVersion.Validate(); err != nil { 93 return fmt.Errorf("broken pre-set project version %q for default plugins: %w", projectVersion, err) 94 } 95 if len(plugins) == 0 { 96 return fmt.Errorf("empty set of plugins provided for project version %q", projectVersion) 97 } 98 for _, p := range plugins { 99 if err := plugin.Validate(p); err != nil { 100 return fmt.Errorf("broken pre-set default plugin %q: %v", plugin.KeyFor(p), err) 101 } 102 if !plugin.SupportsVersion(p, projectVersion) { 103 return fmt.Errorf("default plugin %q doesn't support version %q", plugin.KeyFor(p), projectVersion) 104 } 105 c.defaultPlugins[projectVersion] = append(c.defaultPlugins[projectVersion], plugin.KeyFor(p)) 106 } 107 return nil 108 } 109 } 110 111 // WithDefaultProjectVersion is an Option that sets the CLI's default project version. 112 // 113 // Setting an invalid version results in an error. 114 func WithDefaultProjectVersion(version config.Version) Option { 115 return func(c *CLI) error { 116 if err := version.Validate(); err != nil { 117 return fmt.Errorf("broken pre-set default project version %q: %v", version, err) 118 } 119 c.defaultProjectVersion = version 120 return nil 121 } 122 } 123 124 // WithExtraCommands is an Option that adds extra subcommands to the CLI. 125 // 126 // Adding extra commands that duplicate existing commands results in an error. 127 func WithExtraCommands(cmds ...*cobra.Command) Option { 128 return func(c *CLI) error { 129 // We don't know the commands defined by the CLI yet so we are not checking if the extra commands 130 // conflict with a pre-existing one yet. We do this after creating the base commands. 131 c.extraCommands = append(c.extraCommands, cmds...) 132 return nil 133 } 134 } 135 136 // WithExtraAlphaCommands is an Option that adds extra alpha subcommands to the CLI. 137 // 138 // Adding extra alpha commands that duplicate existing commands results in an error. 139 func WithExtraAlphaCommands(cmds ...*cobra.Command) Option { 140 return func(c *CLI) error { 141 // We don't know the commands defined by the CLI yet so we are not checking if the extra alpha commands 142 // conflict with a pre-existing one yet. We do this after creating the base commands. 143 c.extraAlphaCommands = append(c.extraAlphaCommands, cmds...) 144 return nil 145 } 146 } 147 148 // WithCompletion is an Option that adds the completion subcommand. 149 func WithCompletion() Option { 150 return func(c *CLI) error { 151 c.completionCommand = true 152 return nil 153 } 154 } 155 156 // parseExternalPluginArgs returns the program arguments. 157 func parseExternalPluginArgs() (args []string) { 158 // Loop through os.Args and only get flags and their values that should be passed to the plugins 159 // this also removes the --plugins flag and its values from the list passed to the external plugin 160 for i := range os.Args { 161 if strings.Contains(os.Args[i], "--") && !strings.Contains(os.Args[i], "--plugins") { 162 args = append(args, os.Args[i]) 163 164 // Don't go out of bounds and don't append the next value if it is a flag 165 if i+1 < len(os.Args) && !strings.Contains(os.Args[i+1], "--") { 166 args = append(args, os.Args[i+1]) 167 } 168 } 169 } 170 171 return args 172 } 173 174 // isHostSupported checks whether the host system is supported or not. 175 func isHostSupported(host string) bool { 176 for _, platform := range supportedPlatforms { 177 if host == platform { 178 return true 179 } 180 } 181 return false 182 } 183 184 // getPluginsRoot gets the plugin root path. 185 func getPluginsRoot(host string) (pluginsRoot string, err error) { 186 if !isHostSupported(host) { 187 // freebsd, openbsd, windows... 188 return "", fmt.Errorf("host not supported: %v", host) 189 } 190 191 // if user provides specific path, return 192 if pluginsPath := os.Getenv("EXTERNAL_PLUGINS_PATH"); pluginsPath != "" { 193 // verify if the path actually exists 194 if _, err := os.Stat(pluginsPath); err != nil { 195 if os.IsNotExist(err) { 196 // the path does not exist 197 return "", fmt.Errorf("the specified path %s does not exist", pluginsPath) 198 } 199 // some other error 200 return "", fmt.Errorf("error checking the path: %v", err) 201 } 202 // the path exists 203 return pluginsPath, nil 204 } 205 206 // if no specific path, detects the host system and gets the plugins root based on the host. 207 pluginsRelativePath := filepath.Join("kubebuilder", "plugins") 208 if xdgHome := os.Getenv("XDG_CONFIG_HOME"); xdgHome != "" { 209 return filepath.Join(xdgHome, pluginsRelativePath), nil 210 } 211 212 switch host { 213 case "darwin": 214 logrus.Debugf("Detected host is macOS.") 215 pluginsRoot = filepath.Join("Library", "Application Support", pluginsRelativePath) 216 case "linux": 217 logrus.Debugf("Detected host is Linux.") 218 pluginsRoot = filepath.Join(".config", pluginsRelativePath) 219 } 220 221 userHomeDir, err := os.UserHomeDir() 222 if err != nil { 223 return "", fmt.Errorf("error retrieving home dir: %v", err) 224 } 225 226 return filepath.Join(userHomeDir, pluginsRoot), nil 227 } 228 229 // DiscoverExternalPlugins discovers the external plugins in the plugins root directory 230 // and adds them to external.Plugin. 231 func DiscoverExternalPlugins(fs afero.Fs) (ps []plugin.Plugin, err error) { 232 pluginsRoot, err := retrievePluginsRoot(runtime.GOOS) 233 if err != nil { 234 logrus.Errorf("could not get plugins root: %v", err) 235 return nil, err 236 } 237 238 rootInfo, err := fs.Stat(pluginsRoot) 239 if err != nil { 240 if errors.Is(err, afero.ErrFileNotFound) { 241 logrus.Debugf("External plugins dir %q does not exist, skipping external plugin parsing", pluginsRoot) 242 return nil, nil 243 } 244 return nil, err 245 } 246 if !rootInfo.IsDir() { 247 logrus.Debugf("External plugins path %q is not a directory, skipping external plugin parsing", pluginsRoot) 248 return nil, nil 249 } 250 251 pluginInfos, err := afero.ReadDir(fs, pluginsRoot) 252 if err != nil { 253 return nil, err 254 } 255 256 for _, pluginInfo := range pluginInfos { 257 if !pluginInfo.IsDir() { 258 logrus.Debugf("%q is not a directory so skipping parsing", pluginInfo.Name()) 259 continue 260 } 261 262 versions, err := afero.ReadDir(fs, filepath.Join(pluginsRoot, pluginInfo.Name())) 263 if err != nil { 264 return nil, err 265 } 266 267 for _, version := range versions { 268 if !version.IsDir() { 269 logrus.Debugf("%q is not a directory so skipping parsing", version.Name()) 270 continue 271 } 272 273 pluginFiles, err := afero.ReadDir(fs, filepath.Join(pluginsRoot, pluginInfo.Name(), version.Name())) 274 if err != nil { 275 return nil, err 276 } 277 278 for _, pluginFile := range pluginFiles { 279 // find the executable that matches the same name as info.Name(). 280 // if no match is found, compare the external plugin string name before dot 281 // and match it with info.Name() which is the external plugin root dir. 282 // for example: sample.sh --> sample, externalplugin.py --> externalplugin 283 trimmedPluginName := strings.Split(pluginFile.Name(), ".") 284 if trimmedPluginName[0] == "" { 285 return nil, fmt.Errorf("Invalid plugin name found %q", pluginFile.Name()) 286 } 287 288 if pluginFile.Name() == pluginInfo.Name() || trimmedPluginName[0] == pluginInfo.Name() { 289 // check whether the external plugin is an executable. 290 if !isPluginExectuable(pluginFile.Mode()) { 291 return nil, fmt.Errorf("External plugin %q found in path is not an executable", pluginFile.Name()) 292 } 293 294 ep := external.Plugin{ 295 PName: pluginInfo.Name(), 296 Path: filepath.Join(pluginsRoot, pluginInfo.Name(), version.Name(), pluginFile.Name()), 297 PSupportedProjectVersions: []config.Version{cfgv2.Version, cfgv3.Version}, 298 Args: parseExternalPluginArgs(), 299 } 300 301 if err := ep.PVersion.Parse(version.Name()); err != nil { 302 return nil, err 303 } 304 305 logrus.Printf("Adding external plugin: %s", ep.Name()) 306 307 ps = append(ps, ep) 308 309 } 310 } 311 } 312 313 } 314 315 return ps, nil 316 } 317 318 // isPluginExectuable checks if a plugin is an executable based on the bitmask and returns true or false. 319 func isPluginExectuable(mode fs.FileMode) bool { 320 return mode&0111 != 0 321 }