sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/cli/cmd_helpers.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  	"os"
    23  
    24  	"github.com/spf13/cobra"
    25  
    26  	"sigs.k8s.io/kubebuilder/v3/pkg/config"
    27  	"sigs.k8s.io/kubebuilder/v3/pkg/config/store"
    28  	yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml"
    29  	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
    30  	"sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
    31  	"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
    32  )
    33  
    34  // noResolvedPluginError is returned by subcommands that require a plugin when none was resolved.
    35  type noResolvedPluginError struct{}
    36  
    37  // Error implements error interface.
    38  func (e noResolvedPluginError) Error() string {
    39  	return "no resolved plugin, please verify the project version and plugins specified in flags or configuration file"
    40  }
    41  
    42  // noAvailablePluginError is returned by subcommands that require a plugin when none of their specific type was found.
    43  type noAvailablePluginError struct {
    44  	subcommand string
    45  }
    46  
    47  // Error implements error interface.
    48  func (e noAvailablePluginError) Error() string {
    49  	return fmt.Sprintf("resolved plugins do not provide any %s subcommand", e.subcommand)
    50  }
    51  
    52  // cmdErr updates a cobra command to output error information when executed
    53  // or used with the help flag.
    54  func cmdErr(cmd *cobra.Command, err error) {
    55  	cmd.Long = fmt.Sprintf("%s\nNote: %v", cmd.Long, err)
    56  	cmd.RunE = errCmdFunc(err)
    57  }
    58  
    59  // errCmdFunc returns a cobra RunE function that returns the provided error
    60  func errCmdFunc(err error) func(*cobra.Command, []string) error {
    61  	return func(*cobra.Command, []string) error {
    62  		return err
    63  	}
    64  }
    65  
    66  // keySubcommandTuple represents a pairing of the key of a plugin with a plugin.Subcommand.
    67  type keySubcommandTuple struct {
    68  	key        string
    69  	subcommand plugin.Subcommand
    70  
    71  	// skip will be used to flag subcommands that should be skipped after any hook returned a plugin.ExitError.
    72  	skip bool
    73  }
    74  
    75  // filterSubcommands returns a list of plugin keys and subcommands from a filtered list of resolved plugins.
    76  func (c *CLI) filterSubcommands(
    77  	filter func(plugin.Plugin) bool,
    78  	extract func(plugin.Plugin) plugin.Subcommand,
    79  ) []keySubcommandTuple {
    80  	// Unbundle plugins
    81  	plugins := make([]plugin.Plugin, 0, len(c.resolvedPlugins))
    82  	for _, p := range c.resolvedPlugins {
    83  		if bundle, isBundle := p.(plugin.Bundle); isBundle {
    84  			plugins = append(plugins, bundle.Plugins()...)
    85  		} else {
    86  			plugins = append(plugins, p)
    87  		}
    88  	}
    89  
    90  	tuples := make([]keySubcommandTuple, 0, len(plugins))
    91  	for _, p := range plugins {
    92  		if filter(p) {
    93  			tuples = append(tuples, keySubcommandTuple{
    94  				key:        plugin.KeyFor(p),
    95  				subcommand: extract(p),
    96  			})
    97  		}
    98  	}
    99  	return tuples
   100  }
   101  
   102  // applySubcommandHooks runs the initialization hooks and configures the commands pre-run,
   103  // run, and post-run hooks with the appropriate execution hooks.
   104  func (c *CLI) applySubcommandHooks(
   105  	cmd *cobra.Command,
   106  	subcommands []keySubcommandTuple,
   107  	errorMessage string,
   108  	createConfig bool,
   109  ) {
   110  	// In case we create a new project configuration we need to compute the plugin chain.
   111  	pluginChain := make([]string, 0, len(c.resolvedPlugins))
   112  	if createConfig {
   113  		// We extract the plugin keys again instead of using the ones obtained when filtering subcommands
   114  		// as these plugins are unbundled but we want to keep bundle names in the plugin chain.
   115  		for _, p := range c.resolvedPlugins {
   116  			pluginChain = append(pluginChain, plugin.KeyFor(p))
   117  		}
   118  	}
   119  
   120  	options := initializationHooks(cmd, subcommands, c.metadata())
   121  
   122  	factory := executionHooksFactory{
   123  		fs:             c.fs,
   124  		store:          yamlstore.New(c.fs),
   125  		subcommands:    subcommands,
   126  		errorMessage:   errorMessage,
   127  		projectVersion: c.projectVersion,
   128  		pluginChain:    pluginChain,
   129  	}
   130  	cmd.PreRunE = factory.preRunEFunc(options, createConfig)
   131  	cmd.RunE = factory.runEFunc()
   132  	cmd.PostRunE = factory.postRunEFunc()
   133  }
   134  
   135  // initializationHooks executes update metadata and bind flags plugin hooks.
   136  func initializationHooks(
   137  	cmd *cobra.Command,
   138  	subcommands []keySubcommandTuple,
   139  	meta plugin.CLIMetadata,
   140  ) *resourceOptions {
   141  	// Update metadata hook.
   142  	subcmdMeta := plugin.SubcommandMetadata{
   143  		Description: cmd.Long,
   144  		Examples:    cmd.Example,
   145  	}
   146  	for _, tuple := range subcommands {
   147  		if subcommand, updatesMetadata := tuple.subcommand.(plugin.UpdatesMetadata); updatesMetadata {
   148  			subcommand.UpdateMetadata(meta, &subcmdMeta)
   149  		}
   150  	}
   151  	cmd.Long = subcmdMeta.Description
   152  	cmd.Example = subcmdMeta.Examples
   153  
   154  	// Before binding specific plugin flags, bind common ones.
   155  	requiresResource := false
   156  	for _, tuple := range subcommands {
   157  		if _, requiresResource = tuple.subcommand.(plugin.RequiresResource); requiresResource {
   158  			break
   159  		}
   160  	}
   161  	var options *resourceOptions
   162  	if requiresResource {
   163  		options = bindResourceFlags(cmd.Flags())
   164  	}
   165  
   166  	// Bind flags hook.
   167  	for _, tuple := range subcommands {
   168  		if subcommand, hasFlags := tuple.subcommand.(plugin.HasFlags); hasFlags {
   169  			subcommand.BindFlags(cmd.Flags())
   170  		}
   171  	}
   172  
   173  	return options
   174  }
   175  
   176  type executionHooksFactory struct {
   177  	// fs is the filesystem abstraction to scaffold files to.
   178  	fs machinery.Filesystem
   179  	// store is the backend used to load/save the project configuration.
   180  	store store.Store
   181  	// subcommands are the tuples representing the set of subcommands provided by the resolved plugins.
   182  	subcommands []keySubcommandTuple
   183  	// errorMessage is prepended to returned errors.
   184  	errorMessage string
   185  	// projectVersion is the project version that will be used to create new project configurations.
   186  	// It is only used for initialization.
   187  	projectVersion config.Version
   188  	// pluginChain is the plugin chain configured for this project.
   189  	pluginChain []string
   190  }
   191  
   192  func (factory *executionHooksFactory) forEach(cb func(subcommand plugin.Subcommand) error, errorMessage string) error {
   193  	for i, tuple := range factory.subcommands {
   194  		if tuple.skip {
   195  			continue
   196  		}
   197  
   198  		err := cb(tuple.subcommand)
   199  
   200  		var exitError plugin.ExitError
   201  		switch {
   202  		case err == nil:
   203  			// No error do nothing
   204  		case errors.As(err, &exitError):
   205  			// Exit errors imply that no further hooks of this subcommand should be called, so we flag it to be skipped
   206  			factory.subcommands[i].skip = true
   207  			fmt.Printf("skipping remaining hooks of %q: %s\n", tuple.key, exitError.Reason)
   208  		default:
   209  			// Any other error, wrap it
   210  			return fmt.Errorf("%s: %s %q: %w", factory.errorMessage, errorMessage, tuple.key, err)
   211  		}
   212  	}
   213  
   214  	return nil
   215  }
   216  
   217  // preRunEFunc returns a cobra RunE function that loads the configuration, creates the resource,
   218  // and executes inject config, inject resource, and pre-scaffold hooks.
   219  func (factory *executionHooksFactory) preRunEFunc(
   220  	options *resourceOptions,
   221  	createConfig bool,
   222  ) func(*cobra.Command, []string) error {
   223  	return func(*cobra.Command, []string) error {
   224  		if createConfig {
   225  			// Check if a project configuration is already present.
   226  			if err := factory.store.Load(); err == nil || !errors.Is(err, os.ErrNotExist) {
   227  				return fmt.Errorf("%s: already initialized", factory.errorMessage)
   228  			}
   229  
   230  			// Initialize the project configuration.
   231  			if err := factory.store.New(factory.projectVersion); err != nil {
   232  				return fmt.Errorf("%s: error initializing project configuration: %w", factory.errorMessage, err)
   233  			}
   234  		} else {
   235  			// Load the project configuration.
   236  			if err := factory.store.Load(); os.IsNotExist(err) {
   237  				return fmt.Errorf("%s: unable to find configuration file, project must be initialized",
   238  					factory.errorMessage)
   239  			} else if err != nil {
   240  				return fmt.Errorf("%s: unable to load configuration file: %w", factory.errorMessage, err)
   241  			}
   242  		}
   243  		cfg := factory.store.Config()
   244  
   245  		// Set the pluginChain field.
   246  		if len(factory.pluginChain) != 0 {
   247  			_ = cfg.SetPluginChain(factory.pluginChain)
   248  		}
   249  
   250  		// Create the resource if non-nil options provided
   251  		var res *resource.Resource
   252  		if options != nil {
   253  			// TODO: offer a flag instead of hard-coding project-wide domain
   254  			options.Domain = cfg.GetDomain()
   255  			if err := options.validate(); err != nil {
   256  				return fmt.Errorf("%s: unable to create resource: %w", factory.errorMessage, err)
   257  			}
   258  			res = options.newResource()
   259  		}
   260  
   261  		// Inject config hook.
   262  		if err := factory.forEach(func(subcommand plugin.Subcommand) error {
   263  			if subcommand, requiresConfig := subcommand.(plugin.RequiresConfig); requiresConfig {
   264  				return subcommand.InjectConfig(cfg)
   265  			}
   266  			return nil
   267  		}, "unable to inject the configuration to"); err != nil {
   268  			return err
   269  		}
   270  
   271  		if res != nil {
   272  			// Inject resource hook.
   273  			if err := factory.forEach(func(subcommand plugin.Subcommand) error {
   274  				if subcommand, requiresResource := subcommand.(plugin.RequiresResource); requiresResource {
   275  					return subcommand.InjectResource(res)
   276  				}
   277  				return nil
   278  			}, "unable to inject the resource to"); err != nil {
   279  				return err
   280  			}
   281  
   282  			if err := res.Validate(); err != nil {
   283  				return fmt.Errorf("%s: created invalid resource: %w", factory.errorMessage, err)
   284  			}
   285  		}
   286  
   287  		// Pre-scaffold hook.
   288  		// nolint:revive
   289  		if err := factory.forEach(func(subcommand plugin.Subcommand) error {
   290  			if subcommand, hasPreScaffold := subcommand.(plugin.HasPreScaffold); hasPreScaffold {
   291  				return subcommand.PreScaffold(factory.fs)
   292  			}
   293  			return nil
   294  		}, "unable to run pre-scaffold tasks of"); err != nil {
   295  			return err
   296  		}
   297  
   298  		return nil
   299  	}
   300  }
   301  
   302  // runEFunc returns a cobra RunE function that executes the scaffold hook.
   303  func (factory *executionHooksFactory) runEFunc() func(*cobra.Command, []string) error {
   304  	return func(*cobra.Command, []string) error {
   305  		// Scaffold hook.
   306  		// nolint:revive
   307  		if err := factory.forEach(func(subcommand plugin.Subcommand) error {
   308  			return subcommand.Scaffold(factory.fs)
   309  		}, "unable to scaffold with"); err != nil {
   310  			return err
   311  		}
   312  
   313  		return nil
   314  	}
   315  }
   316  
   317  // postRunEFunc returns a cobra RunE function that saves the configuration
   318  // and executes the post-scaffold hook.
   319  func (factory *executionHooksFactory) postRunEFunc() func(*cobra.Command, []string) error {
   320  	return func(*cobra.Command, []string) error {
   321  		if err := factory.store.Save(); err != nil {
   322  			return fmt.Errorf("%s: unable to save configuration file: %w", factory.errorMessage, err)
   323  		}
   324  
   325  		// Post-scaffold hook.
   326  		// nolint:revive
   327  		if err := factory.forEach(func(subcommand plugin.Subcommand) error {
   328  			if subcommand, hasPostScaffold := subcommand.(plugin.HasPostScaffold); hasPostScaffold {
   329  				return subcommand.PostScaffold()
   330  			}
   331  			return nil
   332  		}, "unable to run post-scaffold tasks of"); err != nil {
   333  			return err
   334  		}
   335  
   336  		return nil
   337  	}
   338  }