sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/plugins/external/helpers.go (about) 1 /* 2 Copyright 2021 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 external 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "io" 24 iofs "io/fs" 25 "os" 26 "os/exec" 27 "path/filepath" 28 "strconv" 29 "strings" 30 31 "github.com/spf13/afero" 32 "github.com/spf13/pflag" 33 "sigs.k8s.io/kubebuilder/v3/pkg/machinery" 34 "sigs.k8s.io/kubebuilder/v3/pkg/plugin" 35 "sigs.k8s.io/kubebuilder/v3/pkg/plugin/external" 36 ) 37 38 var outputGetter ExecOutputGetter = &execOutputGetter{} 39 40 const defaultMetadataTemplate = ` 41 %s is an external plugin for scaffolding files to help with your Operator development. 42 43 For more information on how to use this external plugin, it is recommended to 44 consult the external plugin's documentation. 45 ` 46 47 // ExecOutputGetter is an interface that implements the exec output method. 48 type ExecOutputGetter interface { 49 GetExecOutput(req []byte, path string) ([]byte, error) 50 } 51 52 type execOutputGetter struct{} 53 54 func (e *execOutputGetter) GetExecOutput(request []byte, path string) ([]byte, error) { 55 cmd := exec.Command(path) //nolint:gosec 56 cmd.Stdin = bytes.NewBuffer(request) 57 cmd.Stderr = os.Stderr 58 out, err := cmd.Output() 59 if err != nil { 60 return nil, err 61 } 62 63 return out, nil 64 } 65 66 var currentDirGetter OsWdGetter = &osWdGetter{} 67 68 // OsWdGetter is an interface that implements the get current directory method. 69 type OsWdGetter interface { 70 GetCurrentDir() (string, error) 71 } 72 73 type osWdGetter struct{} 74 75 func (o *osWdGetter) GetCurrentDir() (string, error) { 76 currentDir, err := os.Getwd() 77 if err != nil { 78 return "", fmt.Errorf("error getting current directory: %v", err) 79 } 80 81 return currentDir, nil 82 } 83 84 func makePluginRequest(req external.PluginRequest, path string) (*external.PluginResponse, error) { 85 reqBytes, err := json.Marshal(req) 86 if err != nil { 87 return nil, err 88 } 89 90 out, err := outputGetter.GetExecOutput(reqBytes, path) 91 if err != nil { 92 return nil, err 93 } 94 95 res := external.PluginResponse{} 96 if err := json.Unmarshal(out, &res); err != nil { 97 return nil, err 98 } 99 100 // Error if the plugin failed. 101 if res.Error { 102 return nil, fmt.Errorf(strings.Join(res.ErrorMsgs, "\n")) 103 } 104 105 return &res, nil 106 } 107 108 // getUniverseMap is a helper function that is used to read the current directory to build 109 // the universe map. 110 // It will return a map[string]string where the keys are relative paths to files in the directory 111 // and values are the contents, or an error if an issue occurred while reading one of the files. 112 func getUniverseMap(fs machinery.Filesystem) (map[string]string, error) { 113 universe := map[string]string{} 114 115 err := afero.Walk(fs.FS, ".", func(path string, info iofs.FileInfo, err error) error { 116 if err != nil { 117 return err 118 } 119 120 if info.IsDir() { 121 return nil 122 } 123 124 file, err := fs.FS.Open(path) 125 if err != nil { 126 return err 127 } 128 129 defer func() { 130 if err := file.Close(); err != nil { 131 return 132 } 133 }() 134 135 content, err := io.ReadAll(file) 136 if err != nil { 137 return err 138 } 139 140 universe[path] = string(content) 141 142 return nil 143 }) 144 145 if err != nil { 146 return nil, err 147 } 148 149 return universe, nil 150 } 151 152 func handlePluginResponse(fs machinery.Filesystem, req external.PluginRequest, path string) error { 153 var err error 154 155 req.Universe, err = getUniverseMap(fs) 156 if err != nil { 157 return err 158 } 159 160 res, err := makePluginRequest(req, path) 161 if err != nil { 162 return fmt.Errorf("error making request to external plugin: %w", err) 163 } 164 165 currentDir, err := currentDirGetter.GetCurrentDir() 166 if err != nil { 167 return fmt.Errorf("error getting current directory: %v", err) 168 } 169 170 for filename, data := range res.Universe { 171 path := filepath.Join(currentDir, filename) 172 dir := filepath.Dir(path) 173 174 // create the directory if it does not exist 175 if err := os.MkdirAll(dir, 0o750); err != nil { 176 return fmt.Errorf("error creating the directory: %v", err) 177 } 178 179 f, err := fs.FS.Create(path) 180 if err != nil { 181 return err 182 } 183 184 defer func() { 185 if err := f.Close(); err != nil { 186 return 187 } 188 }() 189 190 if _, err := f.Write([]byte(data)); err != nil { 191 return err 192 } 193 } 194 195 return nil 196 } 197 198 // getExternalPluginFlags is a helper function that is used to get a list of flags from an external plugin. 199 // It will return []Flag if successful or an error if there is an issue attempting to get the list of flags. 200 func getExternalPluginFlags(req external.PluginRequest, path string) ([]external.Flag, error) { 201 req.Universe = map[string]string{} 202 203 res, err := makePluginRequest(req, path) 204 if err != nil { 205 return nil, fmt.Errorf("error making request to external plugin: %w", err) 206 } 207 208 return res.Flags, nil 209 } 210 211 // isBooleanFlag is a helper function to determine if an argument flag is a boolean flag 212 func isBooleanFlag(argIndex int, args []string) bool { 213 return argIndex+1 < len(args) && 214 strings.Contains(args[argIndex+1], "--") || 215 argIndex+1 >= len(args) 216 } 217 218 // bindAllFlags will bind all flags passed into the subcommand by a user 219 func bindAllFlags(fs *pflag.FlagSet, args []string) { 220 defaultFlagDescription := "Kubebuilder could not validate this flag with the external plugin. " + 221 "Consult the external plugin documentation for more information." 222 223 // Bind all flags passed in 224 for i := range args { 225 if strings.Contains(args[i], "--") { 226 flag := strings.Replace(args[i], "--", "", 1) 227 // Check if the flag is a boolean flag 228 if isBooleanFlag(i, args) { 229 _ = fs.Bool(flag, false, defaultFlagDescription) 230 } else { 231 _ = fs.String(flag, "", defaultFlagDescription) 232 } 233 } 234 } 235 } 236 237 // bindSpecificFlags with bind flags that are specified by an external plugin as an allowed flag 238 func bindSpecificFlags(fs *pflag.FlagSet, flags []external.Flag) { 239 // Only bind flags returned by the external plugin 240 for _, flag := range flags { 241 switch flag.Type { 242 case "bool": 243 defaultValue, _ := strconv.ParseBool(flag.Default) 244 _ = fs.Bool(flag.Name, defaultValue, flag.Usage) 245 case "int": 246 defaultValue, _ := strconv.Atoi(flag.Default) 247 _ = fs.Int(flag.Name, defaultValue, flag.Usage) 248 case "float": 249 defaultValue, _ := strconv.ParseFloat(flag.Default, 64) 250 _ = fs.Float64(flag.Name, defaultValue, flag.Usage) 251 default: 252 _ = fs.String(flag.Name, flag.Default, flag.Usage) 253 } 254 } 255 } 256 257 func filterFlags(flags []external.Flag, externalFlagFilters []externalFlagFilterFunc) []external.Flag { 258 filteredFlags := []external.Flag{} 259 for _, flag := range flags { 260 ok := true 261 for _, filter := range externalFlagFilters { 262 if !filter(flag) { 263 ok = false 264 break 265 } 266 } 267 if ok { 268 filteredFlags = append(filteredFlags, flag) 269 } 270 } 271 return filteredFlags 272 } 273 274 func filterArgs(args []string, argFilters []argFilterFunc) []string { 275 filteredArgs := []string{} 276 for _, arg := range args { 277 ok := true 278 for _, filter := range argFilters { 279 if !filter(arg) { 280 ok = false 281 break 282 } 283 } 284 if ok { 285 filteredArgs = append(filteredArgs, arg) 286 } 287 } 288 return filteredArgs 289 } 290 291 type ( 292 externalFlagFilterFunc func(flag external.Flag) bool 293 argFilterFunc func(arg string) bool 294 ) 295 296 var ( 297 // see gvkArgFilter 298 gvkFlagFilter = func(flag external.Flag) bool { 299 return gvkArgFilter(flag.Name) 300 } 301 // gvkFlagFilter filters out any flag named "group", "version", "kind" as 302 // they are already bound by kubebuilder 303 gvkArgFilter = func(arg string) bool { 304 arg = strings.Replace(arg, "--", "", 1) 305 for _, invalidFlagName := range []string{ 306 "group", "version", "kind", 307 } { 308 if arg == invalidFlagName { 309 return false 310 } 311 } 312 return true 313 } 314 315 // see helpArgFilter 316 helpFlagFilter = func(flag external.Flag) bool { 317 return helpArgFilter(flag.Name) 318 } 319 // helpArgFilter filters out any flag named "help" as its already bound 320 helpArgFilter = func(arg string) bool { 321 arg = strings.Replace(arg, "--", "", 1) 322 return !(arg == "help") 323 } 324 ) 325 326 func bindExternalPluginFlags(fs *pflag.FlagSet, subcommand string, path string, args []string) { 327 req := external.PluginRequest{ 328 APIVersion: defaultAPIVersion, 329 Command: "flags", 330 Args: []string{"--" + subcommand}, 331 } 332 333 // Get a list of flags for the init subcommand of the external plugin 334 // If it returns an error, parse all flags passed by the user and let 335 // the external plugin return an unknown flag error. 336 flags, err := getExternalPluginFlags(req, path) 337 338 // Filter Flags based on a set of filters that we do not want. 339 // can be used to filter out non-overridable flags or other 340 // criteria by creating your own filterFlagFunc 341 if err != nil { 342 bindAllFlags(fs, filterArgs(args, []argFilterFunc{ 343 gvkArgFilter, 344 helpArgFilter, 345 })) 346 } else { 347 bindSpecificFlags(fs, filterFlags(flags, []externalFlagFilterFunc{ 348 gvkFlagFilter, 349 helpFlagFilter, 350 })) 351 } 352 } 353 354 // setExternalPluginMetadata is a helper function that sets the subcommand 355 // metadata that is used when the help text is shown for a subcommand. 356 // It will attempt to get the Metadata from the external plugin. If the 357 // external plugin returns no Metadata or an error, a default will be used. 358 func setExternalPluginMetadata(subcommand, path string, subcmdMeta *plugin.SubcommandMetadata) { 359 fileName := filepath.Base(path) 360 subcmdMeta.Description = fmt.Sprintf(defaultMetadataTemplate, fileName[:len(fileName)-len(filepath.Ext(fileName))]) 361 362 res, _ := getExternalPluginMetadata(subcommand, path) 363 364 if res != nil { 365 if res.Description != "" { 366 subcmdMeta.Description = res.Description 367 } 368 369 if res.Examples != "" { 370 subcmdMeta.Examples = res.Examples 371 } 372 } 373 } 374 375 // fetchExternalPluginMetadata performs the actual request to the 376 // external plugin to get the metadata. It returns the metadata 377 // or an error if an error occurs during the fetch process. 378 func getExternalPluginMetadata(subcommand, path string) (*plugin.SubcommandMetadata, error) { 379 req := external.PluginRequest{ 380 APIVersion: defaultAPIVersion, 381 Command: "metadata", 382 Args: []string{"--" + subcommand}, 383 Universe: map[string]string{}, 384 } 385 386 res, err := makePluginRequest(req, path) 387 if err != nil { 388 return nil, fmt.Errorf("error making request to external plugin: %w", err) 389 } 390 391 return &res.Metadata, nil 392 }