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 }