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

kubernetes-sigs / kubebuilder / 24199090977

09 Apr 2026 03:38PM UTC coverage: 82.119% (+0.7%) from 81.426%
24199090977

Pull #5596

github

vitorfloriano
feat(cli): add completion for the plugins flag

The --plugins flag now shows completion for
shows all the available plugins (external ones too!).
Pull Request #5596: ✨ feat(CLI): Add completion for the plugins flag

21 of 24 new or added lines in 1 file covered. (87.5%)

7045 of 8579 relevant lines covered (82.12%)

73.06 hits per line

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

88.4
/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"
43
func getShortKey(fullKey string) string {
2✔
44
        name, version := plugin.SplitKey(fullKey)
2✔
45

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

59
        // Strip common suffixes for cleaner display
60
        // e.g., "deploy-image.go" -> "deploy-image", "kustomize.common" -> "kustomize"
61
        shortName = strings.TrimSuffix(shortName, ".go")
2✔
62
        shortName = strings.TrimSuffix(shortName, ".common")
2✔
63

2✔
64
        if version == "" {
2✔
65
                return shortName
×
66
        }
×
67
        return shortName + "/" + version
2✔
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
72
func getPluginDescription(_ string) string {
3✔
73
        // Fallback for external plugins that don't provide descriptions
3✔
74
        return "External or custom plugin"
3✔
75
}
3✔
76

77
func (c CLI) newRootCmd() *cobra.Command {
27✔
78
        cmd := &cobra.Command{
27✔
79
                Use:     c.commandName,
27✔
80
                Long:    c.description,
27✔
81
                Example: c.rootExamples(),
27✔
82
                RunE: func(cmd *cobra.Command, _ []string) error {
27✔
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✔
92
                                                // Help was requested, show help and stop execution
×
93
                                                cmd.SilenceUsage = true
×
94
                                                cmd.SilenceErrors = true
×
95
                                                _ = cmd.Help()
×
96
                                                return errHelpDisplayed
×
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")
27✔
106
        cobra.CheckErr(cmd.RegisterFlagCompletionFunc(pluginsFlag, c.completionPluginsFlag))
27✔
107

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

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

27✔
115
        return cmd
27✔
116
}
117

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

27✔
122
    %[1]s init --domain <YOUR_DOMAIN>
27✔
123

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

27✔
126
    %[1]s init --plugins=<PLUGIN_KEYS>
27✔
127

27✔
128
Available plugins:
27✔
129

27✔
130
%[2]s
27✔
131

27✔
132
To see which plugins support a specific command:
27✔
133

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

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

144
        return str
27✔
145
}
146

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

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

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

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

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

61✔
176
        for pluginKey, p := range c.plugins {
102✔
177
                // Skip deprecated plugins in help output
41✔
178
                if deprecated, ok := p.(plugin.Deprecated); ok {
80✔
179
                        if deprecated.DeprecationWarning() != "" {
42✔
180
                                continue
3✔
181
                        }
182
                }
183

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

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

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

202
                shortKey := getShortKey(pluginKey)
2✔
203

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

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

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

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

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

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

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

254
        return strings.Join(lines, "\n")
1✔
255
}
256

257
// completePluginsFlag implements cobra.CompletionFunc and is registered
258
// as the flag completion function for --plugins
259
// We should note that flag completion does not work for comma-chained values
260
// but works fine when repeating the flag
261
func (c CLI) completionPluginsFlag(
262
        cmd *cobra.Command,
263
        _ []string,
264
        _ string,
265
) ([]string, cobra.ShellCompDirective) {
1✔
266
        // We filter strings that the user already passed to --plugins,
1✔
267
        // in case the user chains the --plugins flag multiple times,
1✔
268
        alreadyEntered, err := cmd.Flags().GetStringSlice(pluginsFlag)
1✔
269
        if err != nil {
1✔
NEW
270
                cobra.CheckErr(err)
×
NEW
271
        }
×
272

273
        comps := make([]string, 0, len(c.plugins))
1✔
274

1✔
275
        for pluginKey, p := range c.plugins {
4✔
276
                if slices.Contains(alreadyEntered, pluginKey) {
4✔
277
                        continue
1✔
278
                }
279

280
                // We also omit deprecated plugins from completion
281
                if deprecated, ok := p.(plugin.Deprecated); ok {
3✔
282
                        if deprecated.DeprecationWarning() != "" {
2✔
283
                                continue
1✔
284
                        }
285
                }
286

287
                // If the plugin provides a description, we show that
288
                // otherwise, we show the default description
289
                desc := ""
1✔
290
                if describable, ok := p.(plugin.Describable); ok {
1✔
NEW
291
                        desc = describable.Description()
×
292
                } else {
1✔
293
                        desc = getPluginDescription(pluginKey)
1✔
294
                }
1✔
295

296
                comps = append(comps, cobra.CompletionWithDesc(pluginKey, desc))
1✔
297
        }
298

299
        return comps, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
1✔
300
}
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