sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/cli/options_test.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  	"path/filepath"
    24  	"runtime"
    25  
    26  	. "github.com/onsi/ginkgo/v2"
    27  	. "github.com/onsi/gomega"
    28  	"github.com/spf13/afero"
    29  	"github.com/spf13/cobra"
    30  
    31  	"sigs.k8s.io/kubebuilder/v3/pkg/config"
    32  	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
    33  	"sigs.k8s.io/kubebuilder/v3/pkg/model/stage"
    34  	"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
    35  )
    36  
    37  var _ = Describe("Discover external plugins", func() {
    38  	Context("with valid plugins root path", func() {
    39  		var (
    40  			homePath   string = os.Getenv("HOME")
    41  			customPath string = "/tmp/myplugins"
    42  			// store user's original EXTERNAL_PLUGINS_PATH
    43  			originalPluginPath string
    44  			xdghome            string
    45  			// store user's original XDG_CONFIG_HOME
    46  			originalXdghome string
    47  		)
    48  
    49  		When("XDG_CONFIG_HOME is not set and using the $HOME environment variable", func() {
    50  			// store and unset the XDG_CONFIG_HOME
    51  			BeforeEach(func() {
    52  				originalXdghome = os.Getenv("XDG_CONFIG_HOME")
    53  				err := os.Unsetenv("XDG_CONFIG_HOME")
    54  				Expect(err).To(BeNil())
    55  			})
    56  
    57  			AfterEach(func() {
    58  				if originalXdghome != "" {
    59  					// restore the original value
    60  					err := os.Setenv("XDG_CONFIG_HOME", originalXdghome)
    61  					Expect(err).To(BeNil())
    62  				}
    63  			})
    64  
    65  			It("should return the correct path for the darwin OS", func() {
    66  				plgPath, err := getPluginsRoot("darwin")
    67  				Expect(err).To(BeNil())
    68  				Expect(plgPath).To(Equal(fmt.Sprintf("%s/Library/Application Support/kubebuilder/plugins", homePath)))
    69  			})
    70  
    71  			It("should return the correct path for the linux OS", func() {
    72  				plgPath, err := getPluginsRoot("linux")
    73  				Expect(err).To(BeNil())
    74  				Expect(plgPath).To(Equal(fmt.Sprintf("%s/.config/kubebuilder/plugins", homePath)))
    75  			})
    76  
    77  			It("should return error when the host is not darwin / linux", func() {
    78  				plgPath, err := getPluginsRoot("random")
    79  				Expect(plgPath).To(Equal(""))
    80  				Expect(err).ToNot(BeNil())
    81  				Expect(err.Error()).To(ContainSubstring("host not supported"))
    82  			})
    83  		})
    84  
    85  		When("XDG_CONFIG_HOME is set", func() {
    86  			BeforeEach(func() {
    87  				// store and set the XDG_CONFIG_HOME
    88  				originalXdghome = os.Getenv("XDG_CONFIG_HOME")
    89  				err := os.Setenv("XDG_CONFIG_HOME", fmt.Sprintf("%s/.config", homePath))
    90  				Expect(err).To(BeNil())
    91  
    92  				xdghome = os.Getenv("XDG_CONFIG_HOME")
    93  			})
    94  
    95  			AfterEach(func() {
    96  				if originalXdghome != "" {
    97  					// restore the original value
    98  					err := os.Setenv("XDG_CONFIG_HOME", originalXdghome)
    99  					Expect(err).To(BeNil())
   100  				} else {
   101  					// unset if it was originally unset
   102  					err := os.Unsetenv("XDG_CONFIG_HOME")
   103  					Expect(err).To(BeNil())
   104  				}
   105  			})
   106  
   107  			It("should return the correct path for the darwin OS", func() {
   108  				plgPath, err := getPluginsRoot("darwin")
   109  				Expect(err).To(BeNil())
   110  				Expect(plgPath).To(Equal(fmt.Sprintf("%s/kubebuilder/plugins", xdghome)))
   111  			})
   112  
   113  			It("should return the correct path for the linux OS", func() {
   114  				plgPath, err := getPluginsRoot("linux")
   115  				Expect(err).To(BeNil())
   116  				Expect(plgPath).To(Equal(fmt.Sprintf("%s/kubebuilder/plugins", xdghome)))
   117  			})
   118  
   119  			It("should return error when the host is not darwin / linux", func() {
   120  				plgPath, err := getPluginsRoot("random")
   121  				Expect(plgPath).To(Equal(""))
   122  				Expect(err).ToNot(BeNil())
   123  				Expect(err.Error()).To(ContainSubstring("host not supported"))
   124  			})
   125  		})
   126  
   127  		When("using the custom path", func() {
   128  			BeforeEach(func() {
   129  				err := os.MkdirAll(customPath, 0750)
   130  				Expect(err).To(BeNil())
   131  
   132  				// store and set the EXTERNAL_PLUGINS_PATH
   133  				originalPluginPath = os.Getenv("EXTERNAL_PLUGINS_PATH")
   134  				err = os.Setenv("EXTERNAL_PLUGINS_PATH", customPath)
   135  				Expect(err).To(BeNil())
   136  			})
   137  
   138  			AfterEach(func() {
   139  				if originalPluginPath != "" {
   140  					// restore the original value
   141  					err := os.Setenv("EXTERNAL_PLUGINS_PATH", originalPluginPath)
   142  					Expect(err).To(BeNil())
   143  				} else {
   144  					// unset if it was originally unset
   145  					err := os.Unsetenv("EXTERNAL_PLUGINS_PATH")
   146  					Expect(err).To(BeNil())
   147  				}
   148  			})
   149  
   150  			It("should return the user given path for darwin OS", func() {
   151  				plgPath, err := getPluginsRoot("darwin")
   152  				Expect(plgPath).To(Equal(customPath))
   153  				Expect(err).To(BeNil())
   154  			})
   155  
   156  			It("should return the user given path for linux OS", func() {
   157  				plgPath, err := getPluginsRoot("linux")
   158  				Expect(plgPath).To(Equal(customPath))
   159  				Expect(err).To(BeNil())
   160  			})
   161  
   162  			It("should report error when the host is not darwin / linux", func() {
   163  				plgPath, err := getPluginsRoot("random")
   164  				Expect(plgPath).To(Equal(""))
   165  				Expect(err).ToNot(BeNil())
   166  				Expect(err.Error()).To(ContainSubstring("host not supported"))
   167  			})
   168  		})
   169  	})
   170  
   171  	Context("with invalid plugins root path", func() {
   172  		var originalPluginPath string
   173  
   174  		BeforeEach(func() {
   175  			originalPluginPath = os.Getenv("EXTERNAL_PLUGINS_PATH")
   176  			err := os.Setenv("EXTERNAL_PLUGINS_PATH", "/non/existent/path")
   177  			Expect(err).To(BeNil())
   178  		})
   179  
   180  		AfterEach(func() {
   181  			if originalPluginPath != "" {
   182  				// restore the original value
   183  				err := os.Setenv("EXTERNAL_PLUGINS_PATH", originalPluginPath)
   184  				Expect(err).To(BeNil())
   185  			} else {
   186  				// unset if it was originally unset
   187  				err := os.Unsetenv("EXTERNAL_PLUGINS_PATH")
   188  				Expect(err).To(BeNil())
   189  			}
   190  		})
   191  
   192  		It("should return an error for the darwin OS", func() {
   193  			plgPath, err := getPluginsRoot("darwin")
   194  			Expect(err).ToNot(BeNil())
   195  			Expect(plgPath).To(Equal(""))
   196  		})
   197  
   198  		It("should return an error for the linux OS", func() {
   199  			plgPath, err := getPluginsRoot("linux")
   200  			Expect(err).ToNot(BeNil())
   201  			Expect(plgPath).To(Equal(""))
   202  		})
   203  
   204  		It("should return an error when the host is not darwin / linux", func() {
   205  			plgPath, err := getPluginsRoot("random")
   206  			Expect(err).ToNot(BeNil())
   207  			Expect(plgPath).To(Equal(""))
   208  		})
   209  	})
   210  
   211  	Context("when plugin executables exist in the expected plugin directories", func() {
   212  		const (
   213  			filePermissions  os.FileMode = 755
   214  			testPluginScript             = `#!/bin/bash
   215  			echo "This is an external plugin"
   216  			`
   217  		)
   218  
   219  		var (
   220  			pluginFilePath string
   221  			pluginFileName string
   222  			pluginPath     string
   223  			f              afero.File
   224  			fs             machinery.Filesystem
   225  			err            error
   226  		)
   227  
   228  		BeforeEach(func() {
   229  			fs = machinery.Filesystem{
   230  				FS: afero.NewMemMapFs(),
   231  			}
   232  
   233  			pluginPath, err = getPluginsRoot(runtime.GOOS)
   234  			Expect(err).To(BeNil())
   235  
   236  			pluginFileName = "externalPlugin.sh"
   237  			pluginFilePath = filepath.Join(pluginPath, "externalPlugin", "v1", pluginFileName)
   238  
   239  			err = fs.FS.MkdirAll(filepath.Dir(pluginFilePath), 0o700)
   240  			Expect(err).To(BeNil())
   241  
   242  			f, err = fs.FS.Create(pluginFilePath)
   243  			Expect(err).To(BeNil())
   244  			Expect(f).ToNot(BeNil())
   245  
   246  			_, err = fs.FS.Stat(pluginFilePath)
   247  			Expect(err).To(BeNil())
   248  		})
   249  
   250  		It("should discover the external plugin executable without any errors", func() {
   251  			// test that DiscoverExternalPlugins works if the plugin file is an executable and
   252  			// is found in the expected path
   253  			_, err = f.WriteString(testPluginScript)
   254  			Expect(err).To(Not(HaveOccurred()))
   255  
   256  			err = fs.FS.Chmod(pluginFilePath, filePermissions)
   257  			Expect(err).To(Not(HaveOccurred()))
   258  
   259  			_, err = fs.FS.Stat(pluginFilePath)
   260  			Expect(err).To(BeNil())
   261  
   262  			ps, err := DiscoverExternalPlugins(fs.FS)
   263  			Expect(err).To(BeNil())
   264  			Expect(ps).NotTo(BeNil())
   265  			Expect(len(ps)).To(Equal(1))
   266  			Expect(ps[0].Name()).To(Equal("externalPlugin"))
   267  			Expect(ps[0].Version().Number).To(Equal(1))
   268  		})
   269  
   270  		It("should discover multiple external plugins and return the plugins without any errors", func() {
   271  			// set the execute permissions on the first plugin executable
   272  			err = fs.FS.Chmod(pluginFilePath, filePermissions)
   273  
   274  			pluginFileName = "myotherexternalPlugin.sh"
   275  			pluginFilePath = filepath.Join(pluginPath, "myotherexternalPlugin", "v1", pluginFileName)
   276  
   277  			f, err = fs.FS.Create(pluginFilePath)
   278  			Expect(err).To(BeNil())
   279  			Expect(f).ToNot(BeNil())
   280  
   281  			_, err = fs.FS.Stat(pluginFilePath)
   282  			Expect(err).To(BeNil())
   283  
   284  			_, err = f.WriteString(testPluginScript)
   285  			Expect(err).To(Not(HaveOccurred()))
   286  
   287  			// set the execute permissions on the second plugin executable
   288  			err = fs.FS.Chmod(pluginFilePath, filePermissions)
   289  			Expect(err).To(Not(HaveOccurred()))
   290  
   291  			_, err = fs.FS.Stat(pluginFilePath)
   292  			Expect(err).To(BeNil())
   293  
   294  			ps, err := DiscoverExternalPlugins(fs.FS)
   295  			Expect(err).To(BeNil())
   296  			Expect(ps).NotTo(BeNil())
   297  			Expect(len(ps)).To(Equal(2))
   298  
   299  			Expect(ps[0].Name()).To(Equal("externalPlugin"))
   300  			Expect(ps[1].Name()).To(Equal("myotherexternalPlugin"))
   301  		})
   302  
   303  		Context("that are invalid", func() {
   304  			BeforeEach(func() {
   305  				fs = machinery.Filesystem{
   306  					FS: afero.NewMemMapFs(),
   307  				}
   308  
   309  				pluginPath, err = getPluginsRoot(runtime.GOOS)
   310  				Expect(err).To(BeNil())
   311  			})
   312  
   313  			It("should error if the plugin found is not an executable", func() {
   314  				pluginFileName = "externalPlugin.sh"
   315  				pluginFilePath = filepath.Join(pluginPath, "externalPlugin", "v1", pluginFileName)
   316  
   317  				err = fs.FS.MkdirAll(filepath.Dir(pluginFilePath), 0o700)
   318  				Expect(err).To(BeNil())
   319  
   320  				f, err := fs.FS.Create(pluginFilePath)
   321  				Expect(err).To(BeNil())
   322  				Expect(f).ToNot(BeNil())
   323  
   324  				_, err = fs.FS.Stat(pluginFilePath)
   325  				Expect(err).To(BeNil())
   326  
   327  				// set the plugin file permissions to read-only
   328  				err = fs.FS.Chmod(pluginFilePath, 0o444)
   329  				Expect(err).To(Not(HaveOccurred()))
   330  
   331  				ps, err := DiscoverExternalPlugins(fs.FS)
   332  				Expect(err).NotTo(BeNil())
   333  				Expect(err.Error()).To(ContainSubstring("not an executable"))
   334  				Expect(len(ps)).To(Equal(0))
   335  			})
   336  
   337  			It("should error if the plugin found has an invalid plugin name", func() {
   338  				pluginFileName = ".sh"
   339  				pluginFilePath = filepath.Join(pluginPath, "externalPlugin", "v1", pluginFileName)
   340  
   341  				err = fs.FS.MkdirAll(filepath.Dir(pluginFilePath), 0o700)
   342  				Expect(err).To(BeNil())
   343  
   344  				f, err = fs.FS.Create(pluginFilePath)
   345  				Expect(err).To(BeNil())
   346  				Expect(f).ToNot(BeNil())
   347  
   348  				ps, err := DiscoverExternalPlugins(fs.FS)
   349  				Expect(err).NotTo(BeNil())
   350  				Expect(err.Error()).To(ContainSubstring("Invalid plugin name found"))
   351  				Expect(len(ps)).To(Equal(0))
   352  			})
   353  		})
   354  
   355  		Context("that does not match the plugin root directory name", func() {
   356  			BeforeEach(func() {
   357  				fs = machinery.Filesystem{
   358  					FS: afero.NewMemMapFs(),
   359  				}
   360  
   361  				pluginPath, err = getPluginsRoot(runtime.GOOS)
   362  				Expect(err).To(BeNil())
   363  			})
   364  
   365  			It("should skip adding the external plugin and not return any errors", func() {
   366  				pluginFileName = "random.sh"
   367  				pluginFilePath = filepath.Join(pluginPath, "externalPlugin", "v1", pluginFileName)
   368  
   369  				err = fs.FS.MkdirAll(filepath.Dir(pluginFilePath), 0o700)
   370  				Expect(err).To(BeNil())
   371  
   372  				f, err = fs.FS.Create(pluginFilePath)
   373  				Expect(err).To(BeNil())
   374  				Expect(f).ToNot(BeNil())
   375  
   376  				err = fs.FS.Chmod(pluginFilePath, filePermissions)
   377  				Expect(err).To(BeNil())
   378  
   379  				ps, err := DiscoverExternalPlugins(fs.FS)
   380  				Expect(err).To(BeNil())
   381  				Expect(len(ps)).To(Equal(0))
   382  			})
   383  
   384  			It("should fail if pluginsroot is empty", func() {
   385  				errPluginsRoot := errors.New("could not retrieve plugins root")
   386  				retrievePluginsRoot = func(host string) (string, error) {
   387  					return "", errPluginsRoot
   388  				}
   389  
   390  				_, err := DiscoverExternalPlugins(fs.FS)
   391  				Expect(err).NotTo(BeNil())
   392  
   393  				Expect(err).To(Equal(errPluginsRoot))
   394  			})
   395  
   396  			It("should skip parsing of directories if plugins root is not a directory", func() {
   397  				retrievePluginsRoot = func(host string) (string, error) {
   398  					return "externalplugin.sh", nil
   399  				}
   400  
   401  				_, err := DiscoverExternalPlugins(fs.FS)
   402  				Expect(err).To(BeNil())
   403  			})
   404  
   405  			It("should return full path to the external plugins without XDG_CONFIG_HOME", func() {
   406  				if _, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok {
   407  					err = os.Setenv("XDG_CONFIG_HOME", "")
   408  					Expect(err).To(BeNil())
   409  				}
   410  
   411  				home := os.Getenv("HOME")
   412  
   413  				pluginsRoot, err := getPluginsRoot("darwin")
   414  				Expect(err).To(BeNil())
   415  				expected := filepath.Join(home, "Library", "Application Support", "kubebuilder", "plugins")
   416  				Expect(pluginsRoot).To(Equal(expected))
   417  
   418  				pluginsRoot, err = getPluginsRoot("linux")
   419  				Expect(err).To(BeNil())
   420  				expected = filepath.Join(home, ".config", "kubebuilder", "plugins")
   421  				Expect(pluginsRoot).To(Equal(expected))
   422  			})
   423  
   424  			It("should return full path to the external plugins with XDG_CONFIG_HOME", func() {
   425  				err = os.Setenv("XDG_CONFIG_HOME", "/some/random/path")
   426  				Expect(err).To(BeNil())
   427  
   428  				pluginsRoot, err := getPluginsRoot(runtime.GOOS)
   429  				Expect(err).To(BeNil())
   430  				Expect(pluginsRoot).To(Equal("/some/random/path/kubebuilder/plugins"))
   431  			})
   432  
   433  			It("should return error when home directory is set to empty", func() {
   434  				_, ok := os.LookupEnv("XDG_CONFIG_HOME")
   435  				if !ok {
   436  				} else {
   437  					err = os.Setenv("XDG_CONFIG_HOME", "")
   438  					Expect(err).To(BeNil())
   439  				}
   440  
   441  				_, ok = os.LookupEnv("HOME")
   442  				if !ok {
   443  				} else {
   444  					err = os.Setenv("HOME", "")
   445  					Expect(err).To(BeNil())
   446  				}
   447  
   448  				pluginsroot, err := getPluginsRoot(runtime.GOOS)
   449  				Expect(err).NotTo(BeNil())
   450  				Expect(pluginsroot).To(Equal(""))
   451  				Expect(err.Error()).To(ContainSubstring("error retrieving home dir"))
   452  			})
   453  		})
   454  	})
   455  
   456  	Context("parsing flags for external plugins", func() {
   457  		It("should only parse flags excluding the `--plugins` flag", func() {
   458  			// change the os.Args for this test and set them back after
   459  			oldArgs := os.Args
   460  			defer func() { os.Args = oldArgs }()
   461  			os.Args = []string{
   462  				"kubebuilder",
   463  				"init",
   464  				"--plugins",
   465  				"myexternalplugin/v1",
   466  				"--domain",
   467  				"example.com",
   468  				"--binary-flag",
   469  				"--license",
   470  				"apache2",
   471  				"--another-binary",
   472  			}
   473  
   474  			args := parseExternalPluginArgs()
   475  			Expect(args).Should(ContainElements(
   476  				"--domain",
   477  				"example.com",
   478  				"--binary-flag",
   479  				"--license",
   480  				"apache2",
   481  				"--another-binary",
   482  			))
   483  
   484  			Expect(args).ShouldNot(ContainElements(
   485  				"kubebuilder",
   486  				"init",
   487  				"--plugins",
   488  				"myexternalplugin/v1",
   489  			))
   490  		})
   491  	})
   492  })
   493  
   494  var _ = Describe("CLI options", func() {
   495  	const (
   496  		pluginName    = "plugin"
   497  		pluginVersion = "v1"
   498  	)
   499  
   500  	var (
   501  		c   *CLI
   502  		err error
   503  
   504  		projectVersion = config.Version{Number: 1}
   505  
   506  		p   = newMockPlugin(pluginName, pluginVersion, projectVersion)
   507  		np1 = newMockPlugin("Plugin", pluginVersion, projectVersion)
   508  		np2 = mockPlugin{pluginName, plugin.Version{Number: -1}, []config.Version{projectVersion}}
   509  		np3 = newMockPlugin(pluginName, pluginVersion)
   510  		np4 = newMockPlugin(pluginName, pluginVersion, config.Version{})
   511  	)
   512  
   513  	Context("WithCommandName", func() {
   514  		It("should use provided command name", func() {
   515  			commandName := "other-command"
   516  			c, err = newCLI(WithCommandName(commandName))
   517  			Expect(err).NotTo(HaveOccurred())
   518  			Expect(c).NotTo(BeNil())
   519  			Expect(c.commandName).To(Equal(commandName))
   520  		})
   521  	})
   522  
   523  	Context("WithVersion", func() {
   524  		It("should use the provided version string", func() {
   525  			version := "Version: 0.0.0"
   526  			c, err = newCLI(WithVersion(version))
   527  			Expect(err).NotTo(HaveOccurred())
   528  			Expect(c).NotTo(BeNil())
   529  			Expect(c.version).To(Equal(version))
   530  		})
   531  	})
   532  
   533  	Context("WithDescription", func() {
   534  		It("should use the provided description string", func() {
   535  			description := "alternative description"
   536  			c, err = newCLI(WithDescription(description))
   537  			Expect(err).NotTo(HaveOccurred())
   538  			Expect(c).NotTo(BeNil())
   539  			Expect(c.description).To(Equal(description))
   540  		})
   541  	})
   542  
   543  	Context("WithPlugins", func() {
   544  		It("should return a valid CLI", func() {
   545  			c, err = newCLI(WithPlugins(p))
   546  			Expect(err).NotTo(HaveOccurred())
   547  			Expect(c).NotTo(BeNil())
   548  			Expect(c.plugins).To(Equal(map[string]plugin.Plugin{plugin.KeyFor(p): p}))
   549  		})
   550  
   551  		When("providing plugins with same keys", func() {
   552  			It("should return an error", func() {
   553  				_, err = newCLI(WithPlugins(p, p))
   554  				Expect(err).To(HaveOccurred())
   555  			})
   556  		})
   557  
   558  		When("providing plugins with same keys in two steps", func() {
   559  			It("should return an error", func() {
   560  				_, err = newCLI(WithPlugins(p), WithPlugins(p))
   561  				Expect(err).To(HaveOccurred())
   562  			})
   563  		})
   564  
   565  		When("providing a plugin with an invalid name", func() {
   566  			It("should return an error", func() {
   567  				_, err = newCLI(WithPlugins(np1))
   568  				Expect(err).To(HaveOccurred())
   569  			})
   570  		})
   571  
   572  		When("providing a plugin with an invalid version", func() {
   573  			It("should return an error", func() {
   574  				_, err = newCLI(WithPlugins(np2))
   575  				Expect(err).To(HaveOccurred())
   576  			})
   577  		})
   578  
   579  		When("providing a plugin with an empty list of supported versions", func() {
   580  			It("should return an error", func() {
   581  				_, err = newCLI(WithPlugins(np3))
   582  				Expect(err).To(HaveOccurred())
   583  			})
   584  		})
   585  
   586  		When("providing a plugin with an invalid list of supported versions", func() {
   587  			It("should return an error", func() {
   588  				_, err = newCLI(WithPlugins(np4))
   589  				Expect(err).To(HaveOccurred())
   590  			})
   591  		})
   592  	})
   593  
   594  	Context("WithDefaultPlugins", func() {
   595  		It("should return a valid CLI", func() {
   596  			c, err = newCLI(WithDefaultPlugins(projectVersion, p))
   597  			Expect(err).NotTo(HaveOccurred())
   598  			Expect(c).NotTo(BeNil())
   599  			Expect(c.defaultPlugins).To(Equal(map[config.Version][]string{projectVersion: {plugin.KeyFor(p)}}))
   600  		})
   601  
   602  		When("providing an invalid project version", func() {
   603  			It("should return an error", func() {
   604  				_, err = newCLI(WithDefaultPlugins(config.Version{}, p))
   605  				Expect(err).To(HaveOccurred())
   606  			})
   607  		})
   608  
   609  		When("providing an empty set of plugins", func() {
   610  			It("should return an error", func() {
   611  				_, err = newCLI(WithDefaultPlugins(projectVersion))
   612  				Expect(err).To(HaveOccurred())
   613  			})
   614  		})
   615  
   616  		When("providing a plugin with an invalid name", func() {
   617  			It("should return an error", func() {
   618  				_, err = newCLI(WithDefaultPlugins(projectVersion, np1))
   619  				Expect(err).To(HaveOccurred())
   620  			})
   621  		})
   622  
   623  		When("providing a plugin with an invalid version", func() {
   624  			It("should return an error", func() {
   625  				_, err = newCLI(WithDefaultPlugins(projectVersion, np2))
   626  				Expect(err).To(HaveOccurred())
   627  			})
   628  		})
   629  
   630  		When("providing a plugin with an empty list of supported versions", func() {
   631  			It("should return an error", func() {
   632  				_, err = newCLI(WithDefaultPlugins(projectVersion, np3))
   633  				Expect(err).To(HaveOccurred())
   634  			})
   635  		})
   636  
   637  		When("providing a plugin with an invalid list of supported versions", func() {
   638  			It("should return an error", func() {
   639  				_, err = newCLI(WithDefaultPlugins(projectVersion, np4))
   640  				Expect(err).To(HaveOccurred())
   641  			})
   642  		})
   643  
   644  		When("providing a default plugin for an unsupported project version", func() {
   645  			It("should return an error", func() {
   646  				_, err = newCLI(WithDefaultPlugins(config.Version{Number: 2}, p))
   647  				Expect(err).To(HaveOccurred())
   648  			})
   649  		})
   650  	})
   651  
   652  	Context("WithDefaultProjectVersion", func() {
   653  		DescribeTable("should return a valid CLI",
   654  			func(projectVersion config.Version) {
   655  				c, err = newCLI(WithDefaultProjectVersion(projectVersion))
   656  				Expect(err).NotTo(HaveOccurred())
   657  				Expect(c).NotTo(BeNil())
   658  				Expect(c.defaultProjectVersion).To(Equal(projectVersion))
   659  			},
   660  			Entry("for version `2`", config.Version{Number: 2}),
   661  			Entry("for version `3-alpha`", config.Version{Number: 3, Stage: stage.Alpha}),
   662  			Entry("for version `3`", config.Version{Number: 3}),
   663  		)
   664  
   665  		DescribeTable("should fail",
   666  			func(projectVersion config.Version) {
   667  				_, err = newCLI(WithDefaultProjectVersion(projectVersion))
   668  				Expect(err).To(HaveOccurred())
   669  			},
   670  			Entry("for empty version", config.Version{}),
   671  			Entry("for invalid stage", config.Version{Number: 1, Stage: stage.Stage(27)}),
   672  		)
   673  	})
   674  
   675  	Context("WithExtraCommands", func() {
   676  		It("should return a valid CLI with extra commands", func() {
   677  			commandTest := &cobra.Command{
   678  				Use: "example",
   679  			}
   680  			c, err = newCLI(WithExtraCommands(commandTest))
   681  			Expect(err).NotTo(HaveOccurred())
   682  			Expect(c).NotTo(BeNil())
   683  			Expect(c.extraCommands).NotTo(BeNil())
   684  			Expect(len(c.extraCommands)).To(Equal(1))
   685  			Expect(c.extraCommands[0]).NotTo(BeNil())
   686  			Expect(c.extraCommands[0].Use).To(Equal(commandTest.Use))
   687  		})
   688  	})
   689  
   690  	Context("WithExtraAlphaCommands", func() {
   691  		It("should return a valid CLI with extra alpha commands", func() {
   692  			commandTest := &cobra.Command{
   693  				Use: "example",
   694  			}
   695  			c, err = newCLI(WithExtraAlphaCommands(commandTest))
   696  			Expect(err).NotTo(HaveOccurred())
   697  			Expect(c).NotTo(BeNil())
   698  			Expect(c.extraAlphaCommands).NotTo(BeNil())
   699  			Expect(len(c.extraAlphaCommands)).To(Equal(1))
   700  			Expect(c.extraAlphaCommands[0]).NotTo(BeNil())
   701  			Expect(c.extraAlphaCommands[0].Use).To(Equal(commandTest.Use))
   702  		})
   703  	})
   704  
   705  	Context("WithCompletion", func() {
   706  		It("should not add the completion command by default", func() {
   707  			c, err = newCLI()
   708  			Expect(err).NotTo(HaveOccurred())
   709  			Expect(c).NotTo(BeNil())
   710  			Expect(c.completionCommand).To(BeFalse())
   711  		})
   712  
   713  		It("should add the completion command if requested", func() {
   714  			c, err = newCLI(WithCompletion())
   715  			Expect(err).NotTo(HaveOccurred())
   716  			Expect(c).NotTo(BeNil())
   717  			Expect(c.completionCommand).To(BeTrue())
   718  		})
   719  	})
   720  })