sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/cli/cli_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 "fmt" 21 "io" 22 "os" 23 "strings" 24 25 . "github.com/onsi/ginkgo/v2" 26 . "github.com/onsi/gomega" 27 "github.com/spf13/afero" 28 "github.com/spf13/cobra" 29 30 "sigs.k8s.io/kubebuilder/v3/pkg/config" 31 cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" 32 cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" 33 "sigs.k8s.io/kubebuilder/v3/pkg/machinery" 34 "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" 35 "sigs.k8s.io/kubebuilder/v3/pkg/plugin" 36 goPluginV3 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3" 37 ) 38 39 func makeMockPluginsFor(projectVersion config.Version, pluginKeys ...string) []plugin.Plugin { 40 plugins := make([]plugin.Plugin, 0, len(pluginKeys)) 41 for _, key := range pluginKeys { 42 n, v := plugin.SplitKey(key) 43 plugins = append(plugins, newMockPlugin(n, v, projectVersion)) 44 } 45 return plugins 46 } 47 48 func makeMapFor(plugins ...plugin.Plugin) map[string]plugin.Plugin { 49 pluginMap := make(map[string]plugin.Plugin, len(plugins)) 50 for _, p := range plugins { 51 pluginMap[plugin.KeyFor(p)] = p 52 } 53 return pluginMap 54 } 55 56 func setFlag(flag, value string) { 57 os.Args = append(os.Args, "subcommand", "--"+flag, value) 58 } 59 60 func setBoolFlag(flag string) { 61 os.Args = append(os.Args, "subcommand", "--"+flag) 62 } 63 64 // nolint:unparam 65 func setProjectVersionFlag(value string) { 66 setFlag(projectVersionFlag, value) 67 } 68 69 func setPluginsFlag(value string) { 70 setFlag(pluginsFlag, value) 71 } 72 73 func hasSubCommand(cmd *cobra.Command, name string) bool { 74 for _, subcommand := range cmd.Commands() { 75 if subcommand.Name() == name { 76 return true 77 } 78 } 79 return false 80 } 81 82 var _ = Describe("CLI", func() { 83 var ( 84 c *CLI 85 projectVersion = config.Version{Number: 3} 86 ) 87 88 BeforeEach(func() { 89 c = &CLI{ 90 fs: machinery.Filesystem{FS: afero.NewMemMapFs()}, 91 } 92 }) 93 94 Context("buildCmd", func() { 95 var projectFile string 96 97 BeforeEach(func() { 98 projectFile = `domain: zeusville.com 99 layout: go.kubebuilder.io/v3 100 projectName: demo-zeus-operator 101 repo: github.com/jmrodri/demo-zeus-operator 102 resources: 103 - crdVersion: v1 104 group: test 105 kind: Test 106 version: v1 107 version: 3-alpha 108 plugins: 109 manifests.sdk.operatorframework.io/v2: {} 110 ` 111 f, err := c.fs.FS.Create("PROJECT") 112 Expect(err).To(Not(HaveOccurred())) 113 114 _, err = f.WriteString(projectFile) 115 Expect(err).To(Not(HaveOccurred())) 116 }) 117 118 When("reading a 3-alpha config", func() { 119 It("should succeed and set the projectVersion", func() { 120 err := c.buildCmd() 121 Expect(err).To(Not(HaveOccurred())) 122 Expect(c.projectVersion.Compare( 123 config.Version{ 124 Number: 3, 125 Stage: stage.Stable, 126 })).To(Equal(0)) 127 }) 128 It("should fail when stable is not registered ", func() { 129 // overwrite project file with fake 4-alpha 130 f, err := c.fs.FS.OpenFile("PROJECT", os.O_WRONLY, 0) 131 Expect(err).To(Not(HaveOccurred())) 132 _, err = f.WriteString(strings.ReplaceAll(projectFile, "3-alpha", "4-alpha")) 133 Expect(err).To(Not(HaveOccurred())) 134 135 // buildCmd should return an error 136 err = c.buildCmd() 137 Expect(err).To(HaveOccurred()) 138 }) 139 }) 140 }) 141 142 // TODO: test CLI.getInfoFromConfigFile using a mock filesystem 143 144 Context("getInfoFromConfig", func() { 145 When("not having layout field", func() { 146 It("should succeed", func() { 147 pluginChain := []string{"go.kubebuilder.io/v2"} 148 149 projectConfig := cfgv2.New() 150 151 Expect(c.getInfoFromConfig(projectConfig)).To(Succeed()) 152 Expect(c.pluginKeys).To(Equal(pluginChain)) 153 Expect(c.projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) 154 }) 155 }) 156 157 When("having a single plugin in the layout field", func() { 158 It("should succeed", func() { 159 pluginChain := []string{"go.kubebuilder.io/v2"} 160 161 projectConfig := cfgv3.New() 162 Expect(projectConfig.SetPluginChain(pluginChain)).To(Succeed()) 163 164 Expect(c.getInfoFromConfig(projectConfig)).To(Succeed()) 165 Expect(c.pluginKeys).To(Equal(pluginChain)) 166 Expect(c.projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) 167 }) 168 }) 169 170 When("having multiple plugins in the layout field", func() { 171 It("should succeed", func() { 172 pluginChain := []string{"go.kubebuilder.io/v2", "declarative.kubebuilder.io/v1"} 173 174 projectConfig := cfgv3.New() 175 Expect(projectConfig.SetPluginChain(pluginChain)).To(Succeed()) 176 177 Expect(c.getInfoFromConfig(projectConfig)).To(Succeed()) 178 Expect(c.pluginKeys).To(Equal(pluginChain)) 179 Expect(c.projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) 180 }) 181 }) 182 183 When("having invalid plugin keys in the layout field", func() { 184 It("should fail", func() { 185 pluginChain := []string{"_/v1"} 186 187 projectConfig := cfgv3.New() 188 Expect(projectConfig.SetPluginChain(pluginChain)).To(Succeed()) 189 190 Expect(c.getInfoFromConfig(projectConfig)).NotTo(Succeed()) 191 }) 192 }) 193 }) 194 195 Context("getInfoFromFlags", func() { 196 // Save os.Args and restore it for every test 197 var args []string 198 BeforeEach(func() { 199 c.cmd = c.newRootCmd() 200 201 args = os.Args 202 }) 203 AfterEach(func() { 204 os.Args = args 205 }) 206 207 When("no flag is set", func() { 208 It("should succeed", func() { 209 Expect(c.getInfoFromFlags(false)).To(Succeed()) 210 Expect(c.pluginKeys).To(BeEmpty()) 211 Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) 212 }) 213 }) 214 215 When(fmt.Sprintf("--%s flag is set", pluginsFlag), func() { 216 It("should succeed using one plugin key", func() { 217 pluginKeys := []string{"go/v1"} 218 setPluginsFlag(strings.Join(pluginKeys, ",")) 219 220 Expect(c.getInfoFromFlags(false)).To(Succeed()) 221 Expect(c.pluginKeys).To(Equal(pluginKeys)) 222 Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) 223 }) 224 225 It("should succeed using more than one plugin key", func() { 226 pluginKeys := []string{"go/v1", "example/v2", "test/v1"} 227 setPluginsFlag(strings.Join(pluginKeys, ",")) 228 229 Expect(c.getInfoFromFlags(false)).To(Succeed()) 230 Expect(c.pluginKeys).To(Equal(pluginKeys)) 231 Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) 232 }) 233 234 It("should succeed using more than one plugin key with spaces", func() { 235 pluginKeys := []string{"go/v1", "example/v2", "test/v1"} 236 setPluginsFlag(strings.Join(pluginKeys, ", ")) 237 238 Expect(c.getInfoFromFlags(false)).To(Succeed()) 239 Expect(c.pluginKeys).To(Equal(pluginKeys)) 240 Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) 241 }) 242 243 It("should fail for an invalid plugin key", func() { 244 setPluginsFlag("_/v1") 245 246 Expect(c.getInfoFromFlags(false)).NotTo(Succeed()) 247 }) 248 }) 249 250 When(fmt.Sprintf("--%s flag is set", projectVersionFlag), func() { 251 It("should succeed", func() { 252 setProjectVersionFlag(projectVersion.String()) 253 254 Expect(c.getInfoFromFlags(false)).To(Succeed()) 255 Expect(c.pluginKeys).To(BeEmpty()) 256 Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) 257 }) 258 259 It("should fail for an invalid project version", func() { 260 setProjectVersionFlag("v_1") 261 262 Expect(c.getInfoFromFlags(false)).NotTo(Succeed()) 263 }) 264 }) 265 266 When(fmt.Sprintf("--%s and --%s flags are set", pluginsFlag, projectVersionFlag), func() { 267 It("should succeed using one plugin key", func() { 268 pluginKeys := []string{"go/v1"} 269 setPluginsFlag(strings.Join(pluginKeys, ",")) 270 setProjectVersionFlag(projectVersion.String()) 271 272 Expect(c.getInfoFromFlags(false)).To(Succeed()) 273 Expect(c.pluginKeys).To(Equal(pluginKeys)) 274 Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) 275 }) 276 277 It("should succeed using more than one plugin key", func() { 278 pluginKeys := []string{"go/v1", "example/v2", "test/v1"} 279 setPluginsFlag(strings.Join(pluginKeys, ",")) 280 setProjectVersionFlag(projectVersion.String()) 281 282 Expect(c.getInfoFromFlags(false)).To(Succeed()) 283 Expect(c.pluginKeys).To(Equal(pluginKeys)) 284 Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) 285 }) 286 287 It("should succeed using more than one plugin key with spaces", func() { 288 pluginKeys := []string{"go/v1", "example/v2", "test/v1"} 289 setPluginsFlag(strings.Join(pluginKeys, ", ")) 290 setProjectVersionFlag(projectVersion.String()) 291 292 Expect(c.getInfoFromFlags(false)).To(Succeed()) 293 Expect(c.pluginKeys).To(Equal(pluginKeys)) 294 Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) 295 }) 296 }) 297 298 When("additional flags are set", func() { 299 It("should succeed", func() { 300 setFlag("extra-flag", "extra-value") 301 302 Expect(c.getInfoFromFlags(false)).To(Succeed()) 303 }) 304 305 // `--help` is not captured by the allowlist, so we need to special case it 306 It("should not fail for `--help`", func() { 307 setBoolFlag("help") 308 309 Expect(c.getInfoFromFlags(false)).To(Succeed()) 310 }) 311 }) 312 }) 313 314 Context("getInfoFromDefaults", func() { 315 pluginKeys := []string{"go.kubebuilder.io/v2"} 316 317 It("should be a no-op if already have plugin keys", func() { 318 c.pluginKeys = pluginKeys 319 320 c.getInfoFromDefaults() 321 Expect(c.pluginKeys).To(Equal(pluginKeys)) 322 Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) 323 }) 324 325 It("should succeed if default plugins for project version are set", func() { 326 c.projectVersion = projectVersion 327 c.defaultPlugins = map[config.Version][]string{projectVersion: pluginKeys} 328 329 c.getInfoFromDefaults() 330 Expect(c.pluginKeys).To(Equal(pluginKeys)) 331 Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) 332 }) 333 334 It("should succeed if default plugins for default project version are set", func() { 335 c.defaultPlugins = map[config.Version][]string{projectVersion: pluginKeys} 336 c.defaultProjectVersion = projectVersion 337 338 c.getInfoFromDefaults() 339 Expect(c.pluginKeys).To(Equal(pluginKeys)) 340 Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) 341 }) 342 343 It("should succeed if default plugins for only a single project version are set", func() { 344 c.defaultPlugins = map[config.Version][]string{projectVersion: pluginKeys} 345 346 c.getInfoFromDefaults() 347 Expect(c.pluginKeys).To(Equal(pluginKeys)) 348 Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) 349 }) 350 }) 351 352 Context("resolvePlugins", func() { 353 pluginKeys := []string{ 354 "foo.example.com/v1", 355 "bar.example.com/v1", 356 "baz.example.com/v1", 357 "foo.kubebuilder.io/v1", 358 "foo.kubebuilder.io/v2", 359 "bar.kubebuilder.io/v1", 360 "bar.kubebuilder.io/v2", 361 } 362 363 plugins := makeMockPluginsFor(projectVersion, pluginKeys...) 364 plugins = append(plugins, 365 newMockPlugin("invalid.kubebuilder.io", "v1"), 366 newMockPlugin("only1.kubebuilder.io", "v1", 367 config.Version{Number: 1}), 368 newMockPlugin("only2.kubebuilder.io", "v1", 369 config.Version{Number: 2}), 370 newMockPlugin("1and2.kubebuilder.io", "v1", 371 config.Version{Number: 1}, config.Version{Number: 2}), 372 newMockPlugin("2and3.kubebuilder.io", "v1", 373 config.Version{Number: 2}, config.Version{Number: 3}), 374 newMockPlugin("1-2and3.kubebuilder.io", "v1", 375 config.Version{Number: 1}, config.Version{Number: 2}, config.Version{Number: 3}), 376 ) 377 pluginMap := makeMapFor(plugins...) 378 379 BeforeEach(func() { 380 c.plugins = pluginMap 381 }) 382 383 DescribeTable("should resolve", 384 func(key, qualified string) { 385 c.pluginKeys = []string{key} 386 c.projectVersion = projectVersion 387 388 Expect(c.resolvePlugins()).To(Succeed()) 389 Expect(len(c.resolvedPlugins)).To(Equal(1)) 390 Expect(plugin.KeyFor(c.resolvedPlugins[0])).To(Equal(qualified)) 391 }, 392 Entry("fully qualified plugin", "foo.example.com/v1", "foo.example.com/v1"), 393 Entry("plugin without version", "foo.example.com", "foo.example.com/v1"), 394 Entry("shortname without version", "baz", "baz.example.com/v1"), 395 Entry("shortname with version", "foo/v2", "foo.kubebuilder.io/v2"), 396 ) 397 398 DescribeTable("should not resolve", 399 func(key string) { 400 c.pluginKeys = []string{key} 401 c.projectVersion = projectVersion 402 403 Expect(c.resolvePlugins()).NotTo(Succeed()) 404 }, 405 Entry("for an ambiguous version", "foo.kubebuilder.io"), 406 Entry("for an ambiguous name", "foo/v1"), 407 Entry("for an ambiguous name and version", "foo"), 408 Entry("for a non-existent name", "blah"), 409 Entry("for a non-existent version", "foo.example.com/v2"), 410 Entry("for a non-existent version", "foo/v3"), 411 Entry("for a non-existent version", "foo.example.com/v3"), 412 Entry("for a plugin that doesn't support the project version", "invalid.kubebuilder.io/v1"), 413 ) 414 415 It("should succeed if only one common project version is found", func() { 416 c.pluginKeys = []string{"1and2", "2and3"} 417 418 Expect(c.resolvePlugins()).To(Succeed()) 419 Expect(c.projectVersion.Compare(config.Version{Number: 2})).To(Equal(0)) 420 }) 421 422 It("should fail if no common project version is found", func() { 423 c.pluginKeys = []string{"only1", "only2"} 424 425 Expect(c.resolvePlugins()).NotTo(Succeed()) 426 }) 427 428 It("should fail if more than one common project versions are found", func() { 429 c.pluginKeys = []string{"1and2", "1-2and3"} 430 431 Expect(c.resolvePlugins()).NotTo(Succeed()) 432 }) 433 434 It("should succeed if more than one common project versions are found and one is the default", func() { 435 c.pluginKeys = []string{"2and3", "1-2and3"} 436 c.defaultProjectVersion = projectVersion 437 438 Expect(c.resolvePlugins()).To(Succeed()) 439 Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) 440 }) 441 }) 442 443 Context("New", func() { 444 var c *CLI 445 var err error 446 447 When("no option is provided", func() { 448 It("should create a valid CLI", func() { 449 _, err = New() 450 Expect(err).NotTo(HaveOccurred()) 451 }) 452 }) 453 454 // NOTE: Options are extensively tested in their own tests. 455 // The ones tested here ensure better coverage. 456 457 When("providing a version string", func() { 458 It("should create a valid CLI", func() { 459 const version = "version string" 460 c, err = New( 461 WithPlugins(&goPluginV3.Plugin{}), 462 WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), 463 WithVersion(version), 464 ) 465 Expect(err).NotTo(HaveOccurred()) 466 Expect(hasSubCommand(c.cmd, "version")).To(BeTrue()) 467 468 // Test the version command 469 c.cmd.SetArgs([]string{"version"}) 470 // Overwrite stdout to read the output and reset it afterwards 471 r, w, _ := os.Pipe() 472 temp := os.Stdout 473 defer func() { 474 os.Stdout = temp 475 }() 476 os.Stdout = w 477 Expect(c.cmd.Execute()).Should(Succeed()) 478 479 _ = w.Close() 480 481 Expect(err).NotTo(HaveOccurred()) 482 printed, _ := io.ReadAll(r) 483 Expect(string(printed)).To(Equal( 484 fmt.Sprintf("%s\n", version))) 485 486 }) 487 }) 488 489 When("enabling completion", func() { 490 It("should create a valid CLI", func() { 491 c, err = New( 492 WithPlugins(&goPluginV3.Plugin{}), 493 WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), 494 WithCompletion(), 495 ) 496 Expect(err).NotTo(HaveOccurred()) 497 Expect(hasSubCommand(c.cmd, "completion")).To(BeTrue()) 498 }) 499 }) 500 501 When("providing an invalid option", func() { 502 It("should return an error", func() { 503 // An empty project version is not valid 504 _, err = New(WithDefaultProjectVersion(config.Version{})) 505 Expect(err).To(HaveOccurred()) 506 }) 507 }) 508 509 When("being unable to resolve plugins", func() { 510 // Save os.Args and restore it for every test 511 var args []string 512 BeforeEach(func() { args = os.Args }) 513 AfterEach(func() { os.Args = args }) 514 515 It("should return a CLI that returns an error", func() { 516 setPluginsFlag("foo") 517 518 c, err = New() 519 Expect(err).NotTo(HaveOccurred()) 520 521 // Overwrite stderr to read the output and reset it afterwards 522 _, w, _ := os.Pipe() 523 temp := os.Stderr 524 defer func() { 525 os.Stderr = temp 526 _ = w.Close() 527 }() 528 os.Stderr = w 529 530 Expect(c.Run()).NotTo(Succeed()) 531 }) 532 }) 533 534 When("providing extra commands", func() { 535 It("should create a valid CLI for non-conflicting ones", func() { 536 extraCommand := &cobra.Command{Use: "extra"} 537 c, err = New( 538 WithPlugins(&goPluginV3.Plugin{}), 539 WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), 540 WithExtraCommands(extraCommand), 541 ) 542 Expect(err).NotTo(HaveOccurred()) 543 Expect(hasSubCommand(c.cmd, extraCommand.Use)).To(BeTrue()) 544 }) 545 546 It("should return an error for conflicting ones", func() { 547 extraCommand := &cobra.Command{Use: "init"} 548 c, err = New( 549 WithPlugins(&goPluginV3.Plugin{}), 550 WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), 551 WithExtraCommands(extraCommand), 552 ) 553 Expect(err).To(HaveOccurred()) 554 }) 555 }) 556 557 When("providing extra alpha commands", func() { 558 It("should create a valid CLI for non-conflicting ones", func() { 559 extraAlphaCommand := &cobra.Command{Use: "extra"} 560 c, err = New( 561 WithPlugins(&goPluginV3.Plugin{}), 562 WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), 563 WithExtraAlphaCommands(extraAlphaCommand), 564 ) 565 Expect(err).NotTo(HaveOccurred()) 566 var alpha *cobra.Command 567 for _, subcmd := range c.cmd.Commands() { 568 if subcmd.Name() == alphaCommand { 569 alpha = subcmd 570 break 571 } 572 } 573 Expect(alpha).NotTo(BeNil()) 574 Expect(hasSubCommand(alpha, extraAlphaCommand.Use)).To(BeTrue()) 575 }) 576 577 It("should return an error for conflicting ones", func() { 578 extraAlphaCommand := &cobra.Command{Use: "extra"} 579 _, err = New( 580 WithPlugins(&goPluginV3.Plugin{}), 581 WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), 582 WithExtraAlphaCommands(extraAlphaCommand, extraAlphaCommand), 583 ) 584 Expect(err).To(HaveOccurred()) 585 }) 586 }) 587 588 When("providing deprecated plugins", func() { 589 It("should succeed and print the deprecation notice", func() { 590 const ( 591 deprecationWarning = "DEPRECATED" 592 ) 593 deprecatedPlugin := newMockDeprecatedPlugin("deprecated", "v1", deprecationWarning, projectVersion) 594 595 // Overwrite stderr to read the deprecation output and reset it afterwards 596 r, w, _ := os.Pipe() 597 temp := os.Stderr 598 defer func() { 599 os.Stderr = temp 600 }() 601 os.Stderr = w 602 603 c, err = New( 604 WithPlugins(deprecatedPlugin), 605 WithDefaultPlugins(projectVersion, deprecatedPlugin), 606 WithDefaultProjectVersion(projectVersion), 607 ) 608 609 _ = w.Close() 610 611 Expect(err).NotTo(HaveOccurred()) 612 printed, _ := io.ReadAll(r) 613 Expect(string(printed)).To(Equal( 614 fmt.Sprintf(noticeColor, fmt.Sprintf(deprecationFmt, deprecationWarning)))) 615 }) 616 }) 617 }) 618 })