• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubernetes-sigs / kubebuilder / 21778493952

07 Feb 2026 10:13AM UTC coverage: 73.863% (-0.09%) from 73.954%
21778493952

Pull #5448

github

camilamacedo86
fix(CLI/API) prevent --help from being validated as plugin name

- Add PersistentPreRunE hook to detect help flags in plugin values
- Filter help flags before plugin validation
- Show plugin descriptions instead of project versions
- Use short plugin keys (go/v4 vs go.kubebuilder.io/v4)
- Filter plugins by subcommand (init/edit/create)
- Hide deprecated plugins from help output
- Add documentation links for each plugin
- Improve root help wording for better getting-started experience

Generate-by: Cursour/Claude
Pull Request #5448: 🐛 fix(CLI/API) prevent --help from being validated as plugin name

174 of 227 new or added lines in 15 files covered. (76.65%)

1 existing line in 1 file now uncovered.

6887 of 9324 relevant lines covered (73.86%)

43.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.24
/pkg/cli/root.go
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 cli
18

19
import (
20
        "errors"
21
        "fmt"
22
        "slices"
23
        "strings"
24

25
        "github.com/spf13/cobra"
26

27
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
28
)
29

30
var (
31
        supportedPlatforms = []string{"darwin", "linux"}
32
        // errHelpDisplayed is returned when help is displayed to prevent command execution
33
        errHelpDisplayed = errors.New("help displayed")
34
)
35

36
// isHelpFlag checks if the given string is a help flag
37
func isHelpFlag(s string) bool {
28✔
38
        return s == "--help" || s == "-h" || s == "help"
28✔
39
}
28✔
40

41
// getShortKey converts a full plugin key to a short display key
42
// Example: "base.go.kubebuilder.io/v4" -> "base.go/v4"
43
func getShortKey(fullKey string) string {
36✔
44
        name, version := plugin.SplitKey(fullKey)
36✔
45

36✔
46
        // Extract the short name (part before .kubebuilder.io or other domain)
36✔
47
        shortName := name
36✔
48
        if strings.Contains(name, ".kubebuilder.io") {
72✔
49
                shortName = strings.TrimSuffix(name, ".kubebuilder.io")
36✔
50
        } else if idx := strings.LastIndex(name, "."); idx > 0 {
36✔
NEW
51
                // For external plugins, try to get a reasonable short name
×
NEW
52
                // Keep the part before the last dot if it looks like a domain
×
NEW
53
                parts := strings.Split(name, ".")
×
NEW
54
                if len(parts) > 2 {
×
NEW
55
                        shortName = strings.Join(parts[:len(parts)-1], ".")
×
NEW
56
                }
×
57
        }
58

59
        if version == "" {
36✔
NEW
60
                return shortName
×
NEW
61
        }
×
62
        return shortName + "/" + version
36✔
63
}
64

65
// getPluginDescription returns a short description for a plugin key
66
// This is a fallback for plugins that don't implement Describable interface
NEW
67
func getPluginDescription(_ string) string {
×
NEW
68
        // Fallback for external plugins that don't provide descriptions
×
NEW
69
        return "External or custom plugin"
×
NEW
70
}
×
71

72
// getPluginDocLink returns the documentation URL for a plugin
73
func getPluginDocLink(pluginKey string) string {
36✔
74
        baseURL := "https://book.kubebuilder.io/plugins/available"
36✔
75

36✔
76
        docLinks := map[string]string{
36✔
77
                "go.kubebuilder.io/v4":                    baseURL + "/go-v4-plugin",
36✔
78
                "base.go.kubebuilder.io/v4":               baseURL + "/go-v4-plugin",
36✔
79
                "kustomize.common.kubebuilder.io/v2":      baseURL + "/kustomize-v2",
36✔
80
                "deploy-image.go.kubebuilder.io/v1-alpha": baseURL + "/deploy-image-plugin-v1-alpha",
36✔
81
                "helm.kubebuilder.io/v2-alpha":            baseURL + "/helm-v2-alpha",
36✔
82
                "helm.kubebuilder.io/v1-alpha":            baseURL + "/helm-v1-alpha",
36✔
83
                "grafana.kubebuilder.io/v1-alpha":         baseURL + "/grafana-v1-alpha",
36✔
84
                "autoupdate.kubebuilder.io/v1-alpha":      baseURL + "/autoupdate-v1-alpha",
36✔
85
        }
36✔
86

36✔
87
        if link, ok := docLinks[pluginKey]; ok {
72✔
88
                return link
36✔
89
        }
36✔
NEW
90
        return "" // No link for external plugins
×
91
}
92

93
func (c CLI) newRootCmd() *cobra.Command {
26✔
94
        cmd := &cobra.Command{
26✔
95
                Use:     c.commandName,
26✔
96
                Long:    c.description,
26✔
97
                Example: c.rootExamples(),
26✔
98
                RunE: func(cmd *cobra.Command, _ []string) error {
26✔
99
                        return cmd.Help()
×
100
                },
×
101
                PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
2✔
102
                        // Check if --plugins flag contains help flags (--help, -h, help)
2✔
103
                        // This handles cases like: kubebuilder init --plugins --help
2✔
104
                        if pluginKeys, err := cmd.Flags().GetStringSlice(pluginsFlag); err == nil {
4✔
105
                                for _, key := range pluginKeys {
4✔
106
                                        key = strings.TrimSpace(key)
2✔
107
                                        if isHelpFlag(key) {
2✔
NEW
108
                                                // Help was requested, show help and stop execution
×
NEW
109
                                                cmd.SilenceUsage = true
×
NEW
110
                                                cmd.SilenceErrors = true
×
NEW
111
                                                _ = cmd.Help()
×
NEW
112
                                                return errHelpDisplayed
×
NEW
113
                                        }
×
114
                                }
115
                        }
116
                        return nil
2✔
117
                },
118
        }
119

120
        // Global flags for all subcommands.
121
        cmd.PersistentFlags().StringSlice(pluginsFlag, nil, "plugin keys to be used for this subcommand execution")
26✔
122

26✔
123
        // Register --project-version on the root command so that it shows up in help.
26✔
124
        cmd.Flags().String(projectVersionFlag, c.defaultProjectVersion.String(), "project version")
26✔
125

26✔
126
        // As the root command will be used to shot the help message under some error conditions,
26✔
127
        // like during plugin resolving, we need to allow unknown flags to prevent parsing errors.
26✔
128
        cmd.FParseErrWhitelist = cobra.FParseErrWhitelist{UnknownFlags: true}
26✔
129

26✔
130
        return cmd
26✔
131
}
132

133
// rootExamples builds the examples string for the root command before resolving plugins
134
func (c CLI) rootExamples() string {
26✔
135
        str := fmt.Sprintf(`Get started by initializing a new project:
26✔
136

26✔
137
    %[1]s init --domain <YOUR_DOMAIN>
26✔
138

26✔
139
The default plugin scaffold includes everything you need. To use optional plugins:
26✔
140

26✔
141
    %[1]s init --plugins=<PLUGIN_KEYS>
26✔
142

26✔
143
Available plugins:
26✔
144

26✔
145
%[2]s
26✔
146

26✔
147
To see which plugins support a specific command:
26✔
148

26✔
149
    %[1]s <init|edit|create> --help
26✔
150
`,
26✔
151
                c.commandName, c.getPluginTable())
26✔
152

26✔
153
        if len(c.defaultPlugins) != 0 {
33✔
154
                if defaultPlugins, found := c.defaultPlugins[c.defaultProjectVersion]; found {
8✔
155
                        str += fmt.Sprintf("\nDefault plugin: %q\n", strings.Join(defaultPlugins, ","))
1✔
156
                }
1✔
157
        }
158

159
        return str
26✔
160
}
161

162
// getPluginTable returns an ASCII table of the available plugins and their supported project versions.
163
func (c CLI) getPluginTable() string {
26✔
164
        return c.getPluginTableFiltered(nil)
26✔
165
}
26✔
166

167
// getPluginTableFiltered returns a formatted list of plugins filtered by a predicate.
168
// If filter is nil, all plugins are included.
169
// Deprecated plugins are automatically excluded from help output.
170
func (c CLI) getPluginTableFiltered(filter func(plugin.Plugin) bool) string {
60✔
171
        type pluginInfo struct {
60✔
172
                shortKey    string
60✔
173
                fullKey     string
60✔
174
                description string
60✔
175
                versions    string
60✔
176
                docLink     string
60✔
177
        }
60✔
178

60✔
179
        plugins := make([]pluginInfo, 0, len(c.plugins))
60✔
180

60✔
181
        for pluginKey, p := range c.plugins {
98✔
182
                // Skip deprecated plugins in help output
38✔
183
                if deprecated, ok := p.(plugin.Deprecated); ok {
76✔
184
                        if deprecated.DeprecationWarning() != "" {
40✔
185
                                continue
2✔
186
                        }
187
                }
188

189
                // Apply filter if provided
190
                if filter != nil && !filter(p) {
36✔
NEW
191
                        continue
×
192
                }
193

194
                shortKey := getShortKey(pluginKey)
36✔
195

36✔
196
                // Get description from plugin if it implements Describable, otherwise use fallback
36✔
197
                var desc string
36✔
198
                if describable, ok := p.(plugin.Describable); ok {
72✔
199
                        desc = describable.Description()
36✔
200
                } else {
36✔
NEW
201
                        desc = getPluginDescription(pluginKey)
×
UNCOV
202
                }
×
203

204
                // Get supported project versions
205
                supportedVersions := p.SupportedProjectVersions()
36✔
206
                versionStrs := make([]string, 0, len(supportedVersions))
36✔
207
                for _, ver := range supportedVersions {
72✔
208
                        versionStrs = append(versionStrs, ver.String())
36✔
209
                }
36✔
210
                versionsStr := strings.Join(versionStrs, ", ")
36✔
211

36✔
212
                // Get documentation link
36✔
213
                docLink := getPluginDocLink(pluginKey)
36✔
214

36✔
215
                plugins = append(plugins, pluginInfo{
36✔
216
                        shortKey:    shortKey,
36✔
217
                        fullKey:     pluginKey,
36✔
218
                        description: desc,
36✔
219
                        versions:    versionsStr,
36✔
220
                        docLink:     docLink,
36✔
221
                })
36✔
222
        }
223

224
        if len(plugins) == 0 {
84✔
225
                return "No plugins available for this subcommand"
24✔
226
        }
24✔
227

228
        // Sort by short key for better readability
229
        slices.SortFunc(plugins, func(a, b pluginInfo) int {
36✔
NEW
230
                return strings.Compare(a.shortKey, b.shortKey)
×
NEW
231
        })
×
232

233
        // Build list-style output
234
        lines := make([]string, 0, len(plugins)*2)
36✔
235
        for _, p := range plugins {
72✔
236
                // Format: ## plugin-key
36✔
237
                //         Description (Supported project versions: X)
36✔
238
                //         More info: https://...
36✔
239
                lines = append(lines, fmt.Sprintf("  %s", p.shortKey))
36✔
240
                descLine := fmt.Sprintf("    %s (Supported project versions: %s)", p.description, p.versions)
36✔
241
                lines = append(lines, descLine)
36✔
242
                if p.docLink != "" {
72✔
243
                        lines = append(lines, fmt.Sprintf("    More info: %s", p.docLink))
36✔
244
                }
36✔
245
                lines = append(lines, "") // Empty line between plugins
36✔
246
        }
247

248
        return strings.Join(lines, "\n")
36✔
249
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc