@@ -22,6 +22,7 @@ import (
2222 . "github.com/onsi/ginkgo/v2"
2323 . "github.com/onsi/gomega"
2424 "github.com/spf13/cobra"
25+ "github.com/spf13/pflag"
2526
2627 "sigs.k8s.io/kubebuilder/v4/pkg/config"
2728 "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
@@ -259,6 +260,183 @@ var _ = Describe("cmd_helpers", func() {
259260 Expect (result ).To (BeEmpty ())
260261 })
261262 })
263+
264+ Context ("duplicate flag handling (mergeFlagSetInto, syncDuplicateFlags)" , func () {
265+ It ("should not panic when merging two FlagSets that define the same flag name (same type)" , func () {
266+ dest := pflag .NewFlagSet ("dest" , pflag .ExitOnError )
267+ src := pflag .NewFlagSet ("src" , pflag .ExitOnError )
268+ duplicateValues := make (map [string ][]pflag.Value )
269+ firstPluginByFlag := make (map [string ]string )
270+
271+ var destBool bool
272+ var srcBool bool
273+ dest .BoolVar (& destBool , "force" , false , "overwrite files (plugin A)" )
274+ src .BoolVar (& srcBool , "force" , false , "regenerate all files (plugin B)" )
275+
276+ err := mergeFlagSetInto (dest , src , duplicateValues , "pluginB/v1" , firstPluginByFlag )
277+ Expect (err ).NotTo (HaveOccurred ())
278+ Expect (dest .Lookup ("force" )).NotTo (BeNil ())
279+ Expect (duplicateValues ["force" ]).To (HaveLen (1 ))
280+ })
281+
282+ It ("should aggregate help text as For plugin (key): desc AND for plugin (key): desc" , func () {
283+ dest := pflag .NewFlagSet ("dest" , pflag .ExitOnError )
284+ src := pflag .NewFlagSet ("src" , pflag .ExitOnError )
285+ duplicateValues := make (map [string ][]pflag.Value )
286+ firstPluginByFlag := make (map [string ]string )
287+
288+ var a , b bool
289+ dest .BoolVar (& a , "force" , false , "overwrite files (plugin A)" )
290+ src .BoolVar (& b , "force" , false , "regenerate all files (plugin B)" )
291+
292+ err := mergeFlagSetInto (dest , src , duplicateValues , "pluginB/v1" , firstPluginByFlag )
293+ Expect (err ).NotTo (HaveOccurred ())
294+
295+ flag := dest .Lookup ("force" )
296+ Expect (flag ).NotTo (BeNil ())
297+ Expect (flag .Usage ).To (ContainSubstring ("overwrite files (plugin A)" ))
298+ Expect (flag .Usage ).To (ContainSubstring ("AND for plugin (pluginB/v1):" ))
299+ Expect (flag .Usage ).To (ContainSubstring ("regenerate all files (plugin B)" ))
300+ })
301+
302+ It ("should prefix first plugin with For plugin (key): when both flags merged via mergeFlagSetInto" , func () {
303+ dest := pflag .NewFlagSet ("dest" , pflag .ExitOnError )
304+ pluginA := pflag .NewFlagSet ("a" , pflag .ExitOnError )
305+ pluginB := pflag .NewFlagSet ("b" , pflag .ExitOnError )
306+ duplicateValues := make (map [string ][]pflag.Value )
307+ firstPluginByFlag := make (map [string ]string )
308+
309+ var a , b bool
310+ pluginA .BoolVar (& a , "force" , false , "overwrite files (plugin A)" )
311+ pluginB .BoolVar (& b , "force" , false , "regenerate all files (plugin B)" )
312+
313+ Expect (mergeFlagSetInto (dest , pluginA , duplicateValues , "pluginA/v1" , firstPluginByFlag )).NotTo (HaveOccurred ())
314+ Expect (mergeFlagSetInto (dest , pluginB , duplicateValues , "pluginB/v1" , firstPluginByFlag )).NotTo (HaveOccurred ())
315+
316+ flag := dest .Lookup ("force" )
317+ Expect (flag ).NotTo (BeNil ())
318+ Expect (flag .Usage ).To (Equal (
319+ "For plugin (pluginA/v1): overwrite files (plugin A) AND for plugin (pluginB/v1): regenerate all files (plugin B)" ))
320+ })
321+
322+ It ("should show full plugin keys in aggregated usage" , func () {
323+ dest := pflag .NewFlagSet ("dest" , pflag .ExitOnError )
324+ goPlugin := pflag .NewFlagSet ("go" , pflag .ExitOnError )
325+ helmPlugin := pflag .NewFlagSet ("helm" , pflag .ExitOnError )
326+ duplicateValues := make (map [string ][]pflag.Value )
327+ firstPluginByFlag := make (map [string ]string )
328+
329+ var a , b bool
330+ goPlugin .BoolVar (& a , "force" , false , "overwrite scaffolded files to apply changes (manual edits may be lost)" )
331+ helmPlugin .BoolVar (& b , "force" , false , "if true, regenerates all the files" )
332+
333+ Expect (mergeFlagSetInto (dest , goPlugin , duplicateValues , "base.go.kubebuilder.io/v4" , firstPluginByFlag )).
334+ NotTo (HaveOccurred ())
335+ Expect (mergeFlagSetInto (dest , helmPlugin , duplicateValues , "helm.kubebuilder.io/v2-alpha" , firstPluginByFlag )).
336+ NotTo (HaveOccurred ())
337+
338+ flag := dest .Lookup ("force" )
339+ Expect (flag ).NotTo (BeNil ())
340+ expectedUsage := "For plugin (base.go.kubebuilder.io/v4): overwrite scaffolded files to apply changes " +
341+ "(manual edits may be lost) AND for plugin (helm.kubebuilder.io/v2-alpha): if true, regenerates all the files"
342+ Expect (flag .Usage ).To (Equal (expectedUsage ))
343+ })
344+
345+ It ("should return error when same flag name is bound with different value types" , func () {
346+ dest := pflag .NewFlagSet ("dest" , pflag .ExitOnError )
347+ src := pflag .NewFlagSet ("src" , pflag .ExitOnError )
348+ duplicateValues := make (map [string ][]pflag.Value )
349+ firstPluginByFlag := make (map [string ]string )
350+ firstPluginByFlag ["flag" ] = "pluginA/v1" // dest already has this flag from a previous plugin
351+
352+ var a bool
353+ var b string
354+ dest .BoolVar (& a , "flag" , false , "bool usage (plugin A)" )
355+ src .StringVar (& b , "flag" , "" , "string usage (plugin B)" )
356+
357+ err := mergeFlagSetInto (dest , src , duplicateValues , "pluginB/v1" , firstPluginByFlag )
358+ Expect (err ).To (HaveOccurred ())
359+ Expect (err .Error ()).To (ContainSubstring ("same flag name" ))
360+ Expect (err .Error ()).To (ContainSubstring ("different value types" ))
361+ Expect (err .Error ()).To (ContainSubstring ("flag" ))
362+ Expect (err .Error ()).To (ContainSubstring ("bool" ))
363+ Expect (err .Error ()).To (ContainSubstring ("string" ))
364+ Expect (err .Error ()).To (ContainSubstring ("pluginA/v1" ))
365+ Expect (err .Error ()).To (ContainSubstring ("pluginB/v1" ))
366+ })
367+
368+ It ("should sync parsed value to duplicate Values after syncDuplicateFlags" , func () {
369+ flags := pflag .NewFlagSet ("cmd" , pflag .ExitOnError )
370+ var mainVal , dupVal bool
371+ flags .BoolVar (& mainVal , "force" , false , "usage" )
372+ tmpFS := pflag .NewFlagSet ("" , pflag .ExitOnError )
373+ tmpFS .BoolVar (& dupVal , "force" , false , "" )
374+ duplicateValues := map [string ][]pflag.Value {
375+ "force" : {tmpFS .Lookup ("force" ).Value },
376+ }
377+
378+ Expect (flags .Parse ([]string {"--force" , "true" })).NotTo (HaveOccurred ())
379+ Expect (mainVal ).To (BeTrue ())
380+ Expect (dupVal ).To (BeFalse ())
381+
382+ syncDuplicateFlags (flags , duplicateValues )
383+ Expect (dupVal ).To (BeTrue ())
384+ })
385+
386+ It ("should give all plugins in the chain the same value for a shared flag (e.g. --force)" , func () {
387+ cmdFlags := pflag .NewFlagSet ("edit" , pflag .ExitOnError )
388+ pluginA := pflag .NewFlagSet ("pluginA" , pflag .ExitOnError )
389+ pluginB := pflag .NewFlagSet ("pluginB" , pflag .ExitOnError )
390+ var forceA , forceB bool
391+ pluginA .BoolVar (& forceA , "force" , false , "plugin A force" )
392+ pluginB .BoolVar (& forceB , "force" , false , "plugin B force" )
393+
394+ duplicateValues := make (map [string ][]pflag.Value )
395+ firstPluginByFlag := make (map [string ]string )
396+ Expect (mergeFlagSetInto (cmdFlags , pluginA , duplicateValues , "pluginA/v1" , firstPluginByFlag )).NotTo (HaveOccurred ())
397+ Expect (mergeFlagSetInto (cmdFlags , pluginB , duplicateValues , "pluginB/v1" , firstPluginByFlag )).NotTo (HaveOccurred ())
398+
399+ Expect (cmdFlags .Parse ([]string {"--force" , "true" })).NotTo (HaveOccurred ())
400+ syncDuplicateFlags (cmdFlags , duplicateValues )
401+ Expect (forceA ).To (BeTrue (), "plugin A must receive the value passed by the user" )
402+ Expect (forceB ).To (BeTrue (), "plugin B must receive the same value as the command" )
403+ })
404+
405+ It ("should sync string flag value to duplicate Values" , func () {
406+ flags := pflag .NewFlagSet ("cmd" , pflag .ExitOnError )
407+ var mainVal , dupVal string
408+ flags .StringVar (& mainVal , "name" , "" , "name usage" )
409+ tmpFS := pflag .NewFlagSet ("" , pflag .ExitOnError )
410+ tmpFS .StringVar (& dupVal , "name" , "" , "" )
411+ duplicateValues := map [string ][]pflag.Value {
412+ "name" : {tmpFS .Lookup ("name" ).Value },
413+ }
414+
415+ Expect (flags .Parse ([]string {"--name" , "foo" })).NotTo (HaveOccurred ())
416+ syncDuplicateFlags (flags , duplicateValues )
417+ Expect (dupVal ).To (Equal ("foo" ))
418+ })
419+
420+ It ("applies merge and sync for any subcommand (init, api, webhook, edit), not only edit" , func () {
421+ cmd := & cobra.Command {Use : "api" }
422+ pluginA := & mockSubcommandWithForceFlag {}
423+ pluginB := & mockSubcommandWithForceFlag {}
424+ tuples := []keySubcommandTuple {
425+ {key : "pluginA.kubebuilder.io/v1" , subcommand : pluginA },
426+ {key : "pluginB.kubebuilder.io/v1" , subcommand : pluginB },
427+ }
428+ meta := plugin.CLIMetadata {}
429+
430+ result , err := initializationHooks (cmd , tuples , meta )
431+ Expect (err ).NotTo (HaveOccurred ())
432+ Expect (result .duplicateFlagValues ["force" ]).To (HaveLen (1 ), "second plugin's Value recorded as duplicate" )
433+
434+ Expect (cmd .ParseFlags ([]string {"--force" , "true" })).NotTo (HaveOccurred ())
435+ syncDuplicateFlags (cmd .Flags (), result .duplicateFlagValues )
436+ Expect (pluginA .Force ).To (BeTrue (), "first plugin (flag on command) receives value" )
437+ Expect (pluginB .Force ).To (BeTrue (), "second plugin (duplicate) receives same value after sync" )
438+ })
439+ })
262440})
263441
264442type mockTestSubcommand struct {}
@@ -267,6 +445,19 @@ func (m *mockTestSubcommand) Scaffold(machinery.Filesystem) error {
267445 return nil
268446}
269447
448+ // mockSubcommandWithForceFlag implements Subcommand and HasFlags for tests with a shared flag.
449+ type mockSubcommandWithForceFlag struct {
450+ Force bool
451+ }
452+
453+ func (m * mockSubcommandWithForceFlag ) Scaffold (machinery.Filesystem ) error {
454+ return nil
455+ }
456+
457+ func (m * mockSubcommandWithForceFlag ) BindFlags (flags * pflag.FlagSet ) {
458+ flags .BoolVar (& m .Force , "force" , false , "force usage" )
459+ }
460+
270461type mockPluginWithSubcommand struct {
271462 name string
272463 supportedProjectVersions []config.Version
0 commit comments