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

kubernetes-sigs / kubebuilder / 21794733234

08 Feb 2026 07:59AM UTC coverage: 73.272% (-0.7%) from 73.954%
21794733234

push

github

web-flow
Merge pull request #5448 from camilamacedo86/help-fix

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

116 of 220 new or added lines in 15 files covered. (52.73%)

3 existing lines in 1 file now uncovered.

6826 of 9316 relevant lines covered (73.27%)

43.28 hits per line

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

51.59
/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: "deploy-image.go.kubebuilder.io/v1-alpha" -> "deploy-image/v1-alpha"
NEW
43
func getShortKey(fullKey string) string {
×
NEW
44
        name, version := plugin.SplitKey(fullKey)
×
NEW
45

×
NEW
46
        // Extract the short name (part before .kubebuilder.io or other domain)
×
NEW
47
        shortName := name
×
NEW
48
        if strings.Contains(name, ".kubebuilder.io") {
×
NEW
49
                shortName = strings.TrimSuffix(name, ".kubebuilder.io")
×
NEW
50
        } else if idx := strings.LastIndex(name, "."); idx > 0 {
×
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
        // Strip common suffixes for cleaner display
60
        // e.g., "deploy-image.go" -> "deploy-image", "kustomize.common" -> "kustomize"
NEW
61
        shortName = strings.TrimSuffix(shortName, ".go")
×
NEW
62
        shortName = strings.TrimSuffix(shortName, ".common")
×
NEW
63

×
NEW
64
        if version == "" {
×
NEW
65
                return shortName
×
NEW
66
        }
×
NEW
67
        return shortName + "/" + version
×
68
}
69

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

77
func (c CLI) newRootCmd() *cobra.Command {
26✔
78
        cmd := &cobra.Command{
26✔
79
                Use:     c.commandName,
26✔
80
                Long:    c.description,
26✔
81
                Example: c.rootExamples(),
26✔
82
                RunE: func(cmd *cobra.Command, _ []string) error {
26✔
83
                        return cmd.Help()
×
84
                },
×
85
                PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
2✔
86
                        // Check if --plugins flag contains help flags (--help, -h, help)
2✔
87
                        // This handles cases like: kubebuilder init --plugins --help
2✔
88
                        if pluginKeys, err := cmd.Flags().GetStringSlice(pluginsFlag); err == nil {
4✔
89
                                for _, key := range pluginKeys {
4✔
90
                                        key = strings.TrimSpace(key)
2✔
91
                                        if isHelpFlag(key) {
2✔
NEW
92
                                                // Help was requested, show help and stop execution
×
NEW
93
                                                cmd.SilenceUsage = true
×
NEW
94
                                                cmd.SilenceErrors = true
×
NEW
95
                                                _ = cmd.Help()
×
NEW
96
                                                return errHelpDisplayed
×
NEW
97
                                        }
×
98
                                }
99
                        }
100
                        return nil
2✔
101
                },
102
        }
103

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

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

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

26✔
114
        return cmd
26✔
115
}
116

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

26✔
121
    %[1]s init --domain <YOUR_DOMAIN>
26✔
122

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

26✔
125
    %[1]s init --plugins=<PLUGIN_KEYS>
26✔
126

26✔
127
Available plugins:
26✔
128

26✔
129
%[2]s
26✔
130

26✔
131
To see which plugins support a specific command:
26✔
132

26✔
133
    %[1]s <init|edit|create> --help
26✔
134
`,
26✔
135
                c.commandName, c.getPluginTable())
26✔
136

26✔
137
        if len(c.defaultPlugins) != 0 {
33✔
138
                if defaultPlugins, found := c.defaultPlugins[c.defaultProjectVersion]; found {
8✔
139
                        str += fmt.Sprintf("\nDefault plugin: %q\n", strings.Join(defaultPlugins, ","))
1✔
140
                }
1✔
141
        }
142

143
        return str
26✔
144
}
145

146
// getPluginTable returns an ASCII table of the available plugins and their supported project versions.
147
func (c CLI) getPluginTable() string {
26✔
148
        return c.getPluginTableFiltered(nil)
26✔
149
}
26✔
150

151
// getPluginTableFilteredForSubcommand returns a filtered list of plugins for subcommands,
152
// excluding the default scaffold bundle and its component plugins.
153
func (c CLI) getPluginTableFilteredForSubcommand(filter func(plugin.Plugin) bool) string {
34✔
154
        return c.getPluginTableFilteredWithOptions(filter, true)
34✔
155
}
34✔
156

157
// getPluginTableFiltered returns a formatted list of plugins filtered by a predicate.
158
// If filter is nil, all plugins are included.
159
// Deprecated plugins are automatically excluded from help output.
160
func (c CLI) getPluginTableFiltered(filter func(plugin.Plugin) bool) string {
26✔
161
        return c.getPluginTableFilteredWithOptions(filter, false)
26✔
162
}
26✔
163

164
// getPluginTableFilteredWithOptions returns a formatted list of plugins with filtering options.
165
func (c CLI) getPluginTableFilteredWithOptions(filter func(plugin.Plugin) bool, excludeDefaultScaffold bool) string {
60✔
166
        type pluginInfo struct {
60✔
167
                shortKey    string
60✔
168
                fullKey     string
60✔
169
                description string
60✔
170
                versions    string
60✔
171
        }
60✔
172

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

60✔
175
        for pluginKey, p := range c.plugins {
98✔
176
                // Skip deprecated plugins in help output
38✔
177
                if deprecated, ok := p.(plugin.Deprecated); ok {
76✔
178
                        if deprecated.DeprecationWarning() != "" {
40✔
179
                                continue
2✔
180
                        }
181
                }
182

183
                // Apply filter if provided
184
                if filter != nil && !filter(p) {
36✔
NEW
185
                        continue
×
186
                }
187

188
                // Skip base.go plugin to avoid duplication with go plugin
189
                if strings.Contains(pluginKey, "base.go.kubebuilder.io") {
72✔
190
                        continue
36✔
191
                }
192

193
                // For subcommands, skip default scaffold and its component plugins
NEW
194
                if excludeDefaultScaffold {
×
NEW
195
                        if pluginKey == "go.kubebuilder.io/v4" ||
×
NEW
196
                                pluginKey == "kustomize.common.kubebuilder.io/v2" {
×
NEW
197
                                continue
×
198
                        }
199
                }
200

NEW
201
                shortKey := getShortKey(pluginKey)
×
NEW
202

×
NEW
203
                // Get description from plugin if it implements Describable, otherwise use fallback
×
NEW
204
                var desc string
×
NEW
205
                if describable, ok := p.(plugin.Describable); ok {
×
NEW
206
                        desc = describable.Description()
×
NEW
207
                } else {
×
NEW
208
                        desc = getPluginDescription(pluginKey)
×
NEW
209
                }
×
210

211
                // Get supported project versions
NEW
212
                supportedVersions := p.SupportedProjectVersions()
×
NEW
213
                versionStrs := make([]string, 0, len(supportedVersions))
×
NEW
214
                for _, ver := range supportedVersions {
×
NEW
215
                        versionStrs = append(versionStrs, ver.String())
×
NEW
216
                }
×
NEW
217
                versionsStr := strings.Join(versionStrs, ", ")
×
NEW
218

×
NEW
219
                plugins = append(plugins, pluginInfo{
×
NEW
220
                        shortKey:    shortKey,
×
NEW
221
                        fullKey:     pluginKey,
×
NEW
222
                        description: desc,
×
NEW
223
                        versions:    versionsStr,
×
NEW
224
                })
×
225
        }
226

227
        if len(plugins) == 0 {
120✔
228
                return "No plugins available for this subcommand"
60✔
229
        }
60✔
230

231
        // Sort by short key for better readability
NEW
232
        slices.SortFunc(plugins, func(a, b pluginInfo) int {
×
NEW
233
                return strings.Compare(a.shortKey, b.shortKey)
×
NEW
234
        })
×
235

236
        // Calculate max width for KEY column
NEW
237
        maxKeyWidth := len("KEY")
×
NEW
238
        for _, p := range plugins {
×
NEW
239
                if len(p.shortKey) > maxKeyWidth {
×
NEW
240
                        maxKeyWidth = len(p.shortKey)
×
UNCOV
241
                }
×
242
        }
243

244
        // Build aligned column output
NEW
245
        lines := make([]string, 0, len(plugins)+1)
×
NEW
246
        // Header
×
NEW
247
        lines = append(lines, fmt.Sprintf("  %-*s  %s", maxKeyWidth, "KEY", "DESCRIPTION"))
×
NEW
248
        // Entries
×
NEW
249
        for _, p := range plugins {
×
NEW
250
                lines = append(lines, fmt.Sprintf("  %-*s  %s", maxKeyWidth, p.shortKey, p.description))
×
UNCOV
251
        }
×
252

UNCOV
253
        return strings.Join(lines, "\n")
×
254
}
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