sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/plugins/external/external_test.go (about) 1 /* 2 Copyright 2022 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 "encoding/json" 21 "fmt" 22 "os" 23 "path/filepath" 24 "testing" 25 26 . "github.com/onsi/ginkgo/v2" 27 . "github.com/onsi/gomega" 28 "github.com/spf13/afero" 29 "github.com/spf13/pflag" 30 31 "sigs.k8s.io/kubebuilder/v3/pkg/machinery" 32 "sigs.k8s.io/kubebuilder/v3/pkg/plugin" 33 "sigs.k8s.io/kubebuilder/v3/pkg/plugin/external" 34 ) 35 36 func TestExternalPlugin(t *testing.T) { 37 RegisterFailHandler(Fail) 38 RunSpecs(t, "Scaffold") 39 } 40 41 type mockValidOutputGetter struct{} 42 43 type mockInValidOutputGetter struct{} 44 45 var _ ExecOutputGetter = &mockValidOutputGetter{} 46 47 func (m *mockValidOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) { 48 return []byte(`{ 49 "command": "init", 50 "error": false, 51 "error_msg": "none", 52 "universe": {"LICENSE": "Apache 2.0 License\n"} 53 }`), nil 54 } 55 56 var _ ExecOutputGetter = &mockInValidOutputGetter{} 57 58 func (m *mockInValidOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) { 59 return nil, fmt.Errorf("error getting exec command output") 60 } 61 62 type mockValidOsWdGetter struct{} 63 64 var _ OsWdGetter = &mockValidOsWdGetter{} 65 66 func (m *mockValidOsWdGetter) GetCurrentDir() (string, error) { 67 return "tmp/externalPlugin", nil 68 } 69 70 type mockInValidOsWdGetter struct{} 71 72 var _ OsWdGetter = &mockInValidOsWdGetter{} 73 74 func (m *mockInValidOsWdGetter) GetCurrentDir() (string, error) { 75 return "", fmt.Errorf("error getting current directory") 76 } 77 78 type mockValidFlagOutputGetter struct{} 79 80 func (m *mockValidFlagOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) { 81 response := external.PluginResponse{ 82 Command: "flag", 83 Error: false, 84 Universe: nil, 85 Flags: getFlags(), 86 } 87 return json.Marshal(response) 88 } 89 90 type mockValidMEOutputGetter struct{} 91 92 func (m *mockValidMEOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) { 93 response := external.PluginResponse{ 94 Command: "metadata", 95 Error: false, 96 Universe: nil, 97 Metadata: getMetadata(), 98 } 99 100 return json.Marshal(response) 101 } 102 103 const ( 104 externalPlugin = "myexternalplugin.sh" 105 floatVal = "float" 106 ) 107 108 var _ = Describe("Run external plugin using Scaffold", func() { 109 Context("with valid mock values", func() { 110 const filePerm os.FileMode = 755 111 var ( 112 pluginFileName string 113 args []string 114 f afero.File 115 fs machinery.Filesystem 116 117 err error 118 ) 119 120 BeforeEach(func() { 121 outputGetter = &mockValidOutputGetter{} 122 currentDirGetter = &mockValidOsWdGetter{} 123 fs = machinery.Filesystem{ 124 FS: afero.NewMemMapFs(), 125 } 126 127 pluginFileName = "externalPlugin.sh" 128 pluginFilePath := filepath.Join("tmp", "externalPlugin", pluginFileName) 129 130 err = fs.FS.MkdirAll(filepath.Dir(pluginFilePath), filePerm) 131 Expect(err).To(BeNil()) 132 133 f, err = fs.FS.Create(pluginFilePath) 134 Expect(err).To(BeNil()) 135 Expect(f).ToNot(BeNil()) 136 137 _, err = fs.FS.Stat(pluginFilePath) 138 Expect(err).To(BeNil()) 139 140 args = []string{"--domain", "example.com"} 141 }) 142 143 AfterEach(func() { 144 filename := filepath.Join("tmp", "externalPlugin", "LICENSE") 145 fileInfo, err := fs.FS.Stat(filename) 146 Expect(err).To(BeNil()) 147 Expect(fileInfo).NotTo(BeNil()) 148 }) 149 150 It("should successfully run init subcommand on the external plugin", func() { 151 i := initSubcommand{ 152 Path: pluginFileName, 153 Args: args, 154 } 155 156 err = i.Scaffold(fs) 157 Expect(err).To(BeNil()) 158 }) 159 160 It("should successfully run edit subcommand on the external plugin", func() { 161 e := editSubcommand{ 162 Path: pluginFileName, 163 Args: args, 164 } 165 166 err = e.Scaffold(fs) 167 Expect(err).To(BeNil()) 168 }) 169 170 It("should successfully run create api subcommand on the external plugin", func() { 171 c := createAPISubcommand{ 172 Path: pluginFileName, 173 Args: args, 174 } 175 176 err = c.Scaffold(fs) 177 Expect(err).To(BeNil()) 178 }) 179 180 It("should successfully run create webhook subcommand on the external plugin", func() { 181 c := createWebhookSubcommand{ 182 Path: pluginFileName, 183 Args: args, 184 } 185 186 err = c.Scaffold(fs) 187 Expect(err).To(BeNil()) 188 }) 189 }) 190 191 Context("with invalid mock values of GetExecOutput() and GetCurrentDir()", func() { 192 var ( 193 pluginFileName string 194 args []string 195 fs machinery.Filesystem 196 err error 197 ) 198 BeforeEach(func() { 199 outputGetter = &mockInValidOutputGetter{} 200 currentDirGetter = &mockValidOsWdGetter{} 201 fs = machinery.Filesystem{ 202 FS: afero.NewMemMapFs(), 203 } 204 205 pluginFileName = externalPlugin 206 args = []string{"--domain", "example.com"} 207 }) 208 209 It("should return error upon running init subcommand on the external plugin", func() { 210 i := initSubcommand{ 211 Path: pluginFileName, 212 Args: args, 213 } 214 215 err = i.Scaffold(fs) 216 Expect(err).NotTo(BeNil()) 217 Expect(err.Error()).To(ContainSubstring("error getting exec command output")) 218 219 outputGetter = &mockValidOutputGetter{} 220 currentDirGetter = &mockInValidOsWdGetter{} 221 222 err = i.Scaffold(fs) 223 Expect(err).NotTo(BeNil()) 224 Expect(err.Error()).To(ContainSubstring("error getting current directory")) 225 }) 226 227 It("should return error upon running edit subcommand on the external plugin", func() { 228 e := editSubcommand{ 229 Path: pluginFileName, 230 Args: args, 231 } 232 233 err = e.Scaffold(fs) 234 Expect(err).NotTo(BeNil()) 235 Expect(err.Error()).To(ContainSubstring("error getting exec command output")) 236 237 outputGetter = &mockValidOutputGetter{} 238 currentDirGetter = &mockInValidOsWdGetter{} 239 240 err = e.Scaffold(fs) 241 Expect(err).NotTo(BeNil()) 242 Expect(err.Error()).To(ContainSubstring("error getting current directory")) 243 }) 244 245 It("should return error upon running create api subcommand on the external plugin", func() { 246 c := createAPISubcommand{ 247 Path: pluginFileName, 248 Args: args, 249 } 250 251 err = c.Scaffold(fs) 252 Expect(err).NotTo(BeNil()) 253 Expect(err.Error()).To(ContainSubstring("error getting exec command output")) 254 255 outputGetter = &mockValidOutputGetter{} 256 currentDirGetter = &mockInValidOsWdGetter{} 257 258 err = c.Scaffold(fs) 259 Expect(err).NotTo(BeNil()) 260 Expect(err.Error()).To(ContainSubstring("error getting current directory")) 261 }) 262 263 It("should return error upon running create webhook subcommand on the external plugin", func() { 264 c := createWebhookSubcommand{ 265 Path: pluginFileName, 266 Args: args, 267 } 268 269 err = c.Scaffold(fs) 270 Expect(err).NotTo(BeNil()) 271 Expect(err.Error()).To(ContainSubstring("error getting exec command output")) 272 273 outputGetter = &mockValidOutputGetter{} 274 currentDirGetter = &mockInValidOsWdGetter{} 275 276 err = c.Scaffold(fs) 277 Expect(err).NotTo(BeNil()) 278 Expect(err.Error()).To(ContainSubstring("error getting current directory")) 279 }) 280 }) 281 282 Context("with successfully getting flags from external plugin", func() { 283 var ( 284 pluginFileName string 285 args []string 286 flagset *pflag.FlagSet 287 288 // Make an array of flags to represent the ones that should be returned in these tests 289 flags = getFlags() 290 291 checkFlagset func() 292 ) 293 BeforeEach(func() { 294 outputGetter = &mockValidFlagOutputGetter{} 295 currentDirGetter = &mockValidOsWdGetter{} 296 297 pluginFileName = externalPlugin 298 args = []string{"--captain", "black-beard", "--sail"} 299 flagset = pflag.NewFlagSet("test", pflag.ContinueOnError) 300 301 checkFlagset = func() { 302 Expect(flagset.HasFlags()).To(BeTrue()) 303 304 for _, flag := range flags { 305 Expect(flagset.Lookup(flag.Name)).NotTo(BeNil()) 306 // we parse floats as float64 Go type so this check will account for that 307 if flag.Type != floatVal { 308 Expect(flagset.Lookup(flag.Name).Value.Type()).To(Equal(flag.Type)) 309 } else { 310 Expect(flagset.Lookup(flag.Name).Value.Type()).To(Equal("float64")) 311 } 312 Expect(flagset.Lookup(flag.Name).Usage).To(Equal(flag.Usage)) 313 Expect(flagset.Lookup(flag.Name).DefValue).To(Equal(flag.Default)) 314 } 315 } 316 }) 317 318 It("should successfully bind external plugin specified flags for `init` subcommand", func() { 319 sc := initSubcommand{ 320 Path: pluginFileName, 321 Args: args, 322 } 323 324 sc.BindFlags(flagset) 325 326 checkFlagset() 327 }) 328 329 It("should successfully bind external plugin specified flags for `create api` subcommand", func() { 330 sc := createAPISubcommand{ 331 Path: pluginFileName, 332 Args: args, 333 } 334 335 sc.BindFlags(flagset) 336 337 checkFlagset() 338 }) 339 340 It("should successfully bind external plugin specified flags for `create webhook` subcommand", func() { 341 sc := createWebhookSubcommand{ 342 Path: pluginFileName, 343 Args: args, 344 } 345 346 sc.BindFlags(flagset) 347 348 checkFlagset() 349 }) 350 351 It("should successfully bind external plugin specified flags for `edit` subcommand", func() { 352 sc := editSubcommand{ 353 Path: pluginFileName, 354 Args: args, 355 } 356 357 sc.BindFlags(flagset) 358 359 checkFlagset() 360 }) 361 }) 362 363 Context("with failure to get flags from external plugin", func() { 364 var ( 365 pluginFileName string 366 args []string 367 flagset *pflag.FlagSet 368 usage string 369 checkFlagset func() 370 ) 371 BeforeEach(func() { 372 outputGetter = &mockInValidOutputGetter{} 373 currentDirGetter = &mockValidOsWdGetter{} 374 375 pluginFileName = externalPlugin 376 args = []string{"--captain", "black-beard", "--sail"} 377 flagset = pflag.NewFlagSet("test", pflag.ContinueOnError) 378 usage = "Kubebuilder could not validate this flag with the external plugin. " + 379 "Consult the external plugin documentation for more information." 380 381 checkFlagset = func() { 382 Expect(flagset.HasFlags()).To(BeTrue()) 383 384 Expect(flagset.Lookup("captain")).NotTo(BeNil()) 385 Expect(flagset.Lookup("captain").Value.Type()).To(Equal("string")) 386 Expect(flagset.Lookup("captain").Usage).To(Equal(usage)) 387 388 Expect(flagset.Lookup("sail")).NotTo(BeNil()) 389 Expect(flagset.Lookup("sail").Value.Type()).To(Equal("bool")) 390 Expect(flagset.Lookup("sail").Usage).To(Equal(usage)) 391 } 392 }) 393 394 It("should successfully bind all user passed flags for `init` subcommand", func() { 395 sc := initSubcommand{ 396 Path: pluginFileName, 397 Args: args, 398 } 399 400 sc.BindFlags(flagset) 401 402 checkFlagset() 403 }) 404 405 It("should successfully bind all user passed flags for `create api` subcommand", func() { 406 sc := createAPISubcommand{ 407 Path: pluginFileName, 408 Args: args, 409 } 410 411 sc.BindFlags(flagset) 412 413 checkFlagset() 414 }) 415 416 It("should successfully bind all user passed flags for `create webhook` subcommand", func() { 417 sc := createWebhookSubcommand{ 418 Path: pluginFileName, 419 Args: args, 420 } 421 422 sc.BindFlags(flagset) 423 424 checkFlagset() 425 }) 426 427 It("should successfully bind all user passed flags for `edit` subcommand", func() { 428 sc := editSubcommand{ 429 Path: pluginFileName, 430 Args: args, 431 } 432 433 sc.BindFlags(flagset) 434 435 checkFlagset() 436 }) 437 }) 438 439 Context("Flag Parsing Filter Functions", func() { 440 It("gvk(Arg/Flag)Filter should filter out (--)group, (--)version, (--)kind", func() { 441 for _, toBeFiltered := range []string{ 442 "group", "version", "kind", 443 } { 444 Expect(gvkArgFilter("--" + toBeFiltered)).To(BeFalse()) 445 Expect(gvkArgFilter(toBeFiltered)).To(BeFalse()) 446 Expect(gvkFlagFilter(external.Flag{Name: "--" + toBeFiltered})).To(BeFalse()) 447 Expect(gvkFlagFilter(external.Flag{Name: "--" + toBeFiltered})).To(BeFalse()) 448 } 449 Expect(gvkArgFilter("somerandomflag")).To(BeTrue()) 450 Expect(gvkFlagFilter(external.Flag{Name: "somerandomflag"})).To(BeTrue()) 451 }) 452 453 It("helpArgFilter should filter out (--)help", func() { 454 Expect(helpArgFilter("--help")).To(BeFalse()) 455 Expect(helpArgFilter("help")).To(BeFalse()) 456 Expect(helpArgFilter("somerandomflag")).To(BeTrue()) 457 Expect(helpFlagFilter(external.Flag{Name: "--help"})).To(BeFalse()) 458 Expect(helpFlagFilter(external.Flag{Name: "help"})).To(BeFalse()) 459 Expect(helpFlagFilter(external.Flag{Name: "somerandomflag"})).To(BeTrue()) 460 }) 461 }) 462 463 Context("Flag Parsing Helper Functions", func() { 464 var ( 465 fs *pflag.FlagSet 466 args = []string{ 467 "--domain", "something.com", 468 "--boolean", 469 "--another", "flag", 470 "--help", 471 "--group", "somegroup", 472 "--kind", "somekind", 473 "--version", "someversion", 474 } 475 forbidden = []string{ 476 "help", "group", "kind", "version", 477 } 478 flags []external.Flag 479 argFilters []argFilterFunc 480 externalFlagFilters []externalFlagFilterFunc 481 ) 482 483 BeforeEach(func() { 484 fs = pflag.NewFlagSet("test", pflag.ContinueOnError) 485 486 flagsToAppend := getFlags() 487 488 flags = make([]external.Flag, len(flagsToAppend)) 489 copy(flags, flagsToAppend) 490 491 argFilters = []argFilterFunc{ 492 gvkArgFilter, helpArgFilter, 493 } 494 externalFlagFilters = []externalFlagFilterFunc{ 495 gvkFlagFilter, helpFlagFilter, 496 } 497 }) 498 499 It("isBooleanFlag should return true if boolean flag provided at index", func() { 500 Expect(isBooleanFlag(2, args)).To(BeTrue()) 501 }) 502 503 It("isBooleanFlag should return false if boolean flag not provided at index", func() { 504 Expect(isBooleanFlag(0, args)).To(BeFalse()) 505 }) 506 507 It("bindAllFlags should bind all flags", func() { 508 usage := "Kubebuilder could not validate this flag with the external plugin. " + 509 "Consult the external plugin documentation for more information." 510 511 bindAllFlags(fs, filterArgs(args, argFilters)) 512 Expect(fs.HasFlags()).To(BeTrue()) 513 Expect(fs.Lookup("domain")).NotTo(BeNil()) 514 Expect(fs.Lookup("domain").Value.Type()).To(Equal("string")) 515 Expect(fs.Lookup("domain").Usage).To(Equal(usage)) 516 Expect(fs.Lookup("boolean")).NotTo(BeNil()) 517 Expect(fs.Lookup("boolean").Value.Type()).To(Equal("bool")) 518 Expect(fs.Lookup("boolean").Usage).To(Equal(usage)) 519 Expect(fs.Lookup("another")).NotTo(BeNil()) 520 Expect(fs.Lookup("another").Value.Type()).To(Equal("string")) 521 Expect(fs.Lookup("another").Usage).To(Equal(usage)) 522 523 By("bindAllFlags not have bound any forbidden flag after filtering") 524 for i := range forbidden { 525 Expect(fs.Lookup(forbidden[i])).To(BeNil()) 526 } 527 }) 528 529 It("bindSpecificFlags should bind all flags in given []Flag", func() { 530 filteredFlags := filterFlags(flags, externalFlagFilters) 531 bindSpecificFlags(fs, filteredFlags) 532 533 Expect(fs.HasFlags()).To(BeTrue()) 534 535 for _, flag := range filteredFlags { 536 Expect(fs.Lookup(flag.Name)).NotTo(BeNil()) 537 // we parse floats as float64 Go type so this check will account for that 538 if flag.Type != floatVal { 539 Expect(fs.Lookup(flag.Name).Value.Type()).To(Equal(flag.Type)) 540 } else { 541 Expect(fs.Lookup(flag.Name).Value.Type()).To(Equal("float64")) 542 } 543 Expect(fs.Lookup(flag.Name).Usage).To(Equal(flag.Usage)) 544 Expect(fs.Lookup(flag.Name).DefValue).To(Equal(flag.Default)) 545 } 546 547 By("bindSpecificFlags not have bound any forbidden flag after filtering") 548 for i := range forbidden { 549 Expect(fs.Lookup(forbidden[i])).To(BeNil()) 550 } 551 }) 552 }) 553 554 // TODO(everettraven): Add tests for an external plugin setting the Metadata and Examples 555 Context("Successfully retrieving metadata and examples from external plugin", func() { 556 var ( 557 pluginFileName string 558 metadata *plugin.SubcommandMetadata 559 checkMetadata func() 560 ) 561 BeforeEach(func() { 562 outputGetter = &mockValidMEOutputGetter{} 563 currentDirGetter = &mockValidOsWdGetter{} 564 565 pluginFileName = externalPlugin 566 metadata = &plugin.SubcommandMetadata{} 567 568 checkMetadata = func() { 569 Expect(metadata.Description).Should(Equal(getMetadata().Description)) 570 Expect(metadata.Examples).Should(Equal(getMetadata().Examples)) 571 } 572 }) 573 574 It("should use the external plugin's metadata and examples for `init` subcommand", func() { 575 sc := initSubcommand{ 576 Path: pluginFileName, 577 Args: nil, 578 } 579 580 sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) 581 582 checkMetadata() 583 }) 584 585 It("should use the external plugin's metadata and examples for `create api` subcommand", func() { 586 sc := createAPISubcommand{ 587 Path: pluginFileName, 588 Args: nil, 589 } 590 591 sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) 592 593 checkMetadata() 594 }) 595 596 It("should use the external plugin's metadata and examples for `create webhook` subcommand", func() { 597 sc := createWebhookSubcommand{ 598 Path: pluginFileName, 599 Args: nil, 600 } 601 602 sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) 603 604 checkMetadata() 605 }) 606 607 It("should use the external plugin's metadata and examples for `edit` subcommand", func() { 608 sc := editSubcommand{ 609 Path: pluginFileName, 610 Args: nil, 611 } 612 613 sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) 614 615 checkMetadata() 616 }) 617 }) 618 619 Context("Failing to retrieve metadata and examples from external plugin", func() { 620 var ( 621 pluginFileName string 622 metadata *plugin.SubcommandMetadata 623 checkMetadata func() 624 ) 625 BeforeEach(func() { 626 outputGetter = &mockInValidOutputGetter{} 627 currentDirGetter = &mockValidOsWdGetter{} 628 629 pluginFileName = externalPlugin 630 metadata = &plugin.SubcommandMetadata{} 631 632 checkMetadata = func() { 633 Expect(metadata.Description).Should(Equal(fmt.Sprintf(defaultMetadataTemplate, "myexternalplugin"))) 634 Expect(metadata.Examples).Should(BeEmpty()) 635 } 636 }) 637 638 It("should use the default metadata and examples for `init` subcommand", func() { 639 sc := initSubcommand{ 640 Path: pluginFileName, 641 Args: nil, 642 } 643 644 sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) 645 646 checkMetadata() 647 }) 648 649 It("should use the default metadata and examples for `create api` subcommand", func() { 650 sc := createAPISubcommand{ 651 Path: pluginFileName, 652 Args: nil, 653 } 654 655 sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) 656 657 checkMetadata() 658 }) 659 660 It("should use the default metadata and examples for `create webhook` subcommand", func() { 661 sc := createWebhookSubcommand{ 662 Path: pluginFileName, 663 Args: nil, 664 } 665 666 sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) 667 668 checkMetadata() 669 }) 670 671 It("should use the default metadata and examples for `edit` subcommand", func() { 672 sc := editSubcommand{ 673 Path: pluginFileName, 674 Args: nil, 675 } 676 677 sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) 678 679 checkMetadata() 680 }) 681 }) 682 683 Context("Helper functions for Sending request to external plugin and parsing response", func() { 684 It("getUniverseMap should return path to content mapping of all files in Filesystem", func() { 685 fs := machinery.Filesystem{ 686 FS: afero.NewMemMapFs(), 687 } 688 689 files := []struct { 690 path string 691 name string 692 content string 693 }{ 694 { 695 path: "./", 696 name: "file", 697 content: "level 0 file", 698 }, 699 { 700 path: "dir/", 701 name: "file", 702 content: "level 1 file", 703 }, 704 { 705 path: "dir/subdir", 706 name: "file", 707 content: "level 2 file", 708 }, 709 } 710 711 // create files in Filesystem 712 for _, file := range files { 713 err := fs.FS.MkdirAll(file.path, 0o700) 714 Expect(err).ToNot(HaveOccurred()) 715 716 f, err := fs.FS.Create(filepath.Join(file.path, file.name)) 717 Expect(err).ToNot(HaveOccurred()) 718 719 _, err = f.Write([]byte(file.content)) 720 Expect(err).ToNot(HaveOccurred()) 721 722 err = f.Close() 723 Expect(err).ToNot(HaveOccurred()) 724 } 725 726 universe, err := getUniverseMap(fs) 727 728 Expect(err).ToNot(HaveOccurred()) 729 Expect(len(universe)).To(Equal(len(files))) 730 731 for _, file := range files { 732 content := universe[filepath.Join(file.path, file.name)] 733 Expect(content).To(Equal(file.content)) 734 } 735 }) 736 }) 737 }) 738 739 func getFlags() []external.Flag { 740 return []external.Flag{ 741 { 742 Name: "captain", 743 Type: "string", 744 Usage: "specify the ship captain", 745 Default: "jack-sparrow", 746 }, 747 { 748 Name: "sail", 749 Type: "bool", 750 Usage: "deploy the sail", 751 Default: "false", 752 }, 753 { 754 Name: "crew-count", 755 Type: "int", 756 Usage: "number of crew members", 757 Default: "123", 758 }, 759 { 760 Name: "treasure-value", 761 Type: "float", 762 Usage: "value of treasure on board the ship", 763 Default: "123.45", 764 }, 765 } 766 } 767 768 func getMetadata() plugin.SubcommandMetadata { 769 return plugin.SubcommandMetadata{ 770 Description: "Test description", 771 Examples: "Test examples", 772 } 773 }