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  }