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

kubernetes-sigs / kubebuilder / 16592912587

29 Jul 2025 09:59AM UTC coverage: 64.089% (-0.06%) from 64.151%
16592912587

Pull #4968

github

Shubhamag12
Migrate from logrus to log/slog

lint fixes
Pull Request #4968: ⚠️ Migrate from logrus to log/slog

6 of 56 new or added lines in 9 files covered. (10.71%)

1 existing line in 1 file now uncovered.

2627 of 4099 relevant lines covered (64.09%)

13.61 hits per line

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

88.0
/pkg/cli/options.go
1
/*
2
Copyright 2020 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
        "io/fs"
23
        "log/slog"
24
        "os"
25
        "path/filepath"
26
        "runtime"
27
        "strings"
28

29
        "github.com/spf13/afero"
30
        "github.com/spf13/cobra"
31

32
        "sigs.k8s.io/kubebuilder/v4/pkg/config"
33
        cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
34
        "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
35
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
36
        "sigs.k8s.io/kubebuilder/v4/pkg/plugins/external"
37
)
38

39
var retrievePluginsRoot = getPluginsRoot
40

41
// Option is a function used as arguments to New in order to configure the resulting CLI.
42
type Option func(*CLI) error
43

44
// WithCommandName is an Option that sets the CLI's root command name.
45
func WithCommandName(name string) Option {
1✔
46
        return func(c *CLI) error {
2✔
47
                c.commandName = name
1✔
48
                return nil
1✔
49
        }
1✔
50
}
51

52
// WithVersion is an Option that defines the version string of the CLI.
53
func WithVersion(version string) Option {
2✔
54
        return func(c *CLI) error {
4✔
55
                c.version = version
2✔
56
                return nil
2✔
57
        }
2✔
58
}
59

60
// WithCliVersion is an Option that defines only the version string of the CLI (no extra info).
61
func WithCliVersion(version string) Option {
×
62
        return func(c *CLI) error {
×
63
                c.cliVersion = version
×
64
                return nil
×
65
        }
×
66
}
67

68
// WithDescription is an Option that sets the CLI's root description.
69
func WithDescription(description string) Option {
1✔
70
        return func(c *CLI) error {
2✔
71
                c.description = description
1✔
72
                return nil
1✔
73
        }
1✔
74
}
75

76
// WithPlugins is an Option that sets the CLI's plugins.
77
//
78
// Specifying any invalid plugin results in an error.
79
func WithPlugins(plugins ...plugin.Plugin) Option {
15✔
80
        return func(c *CLI) error {
30✔
81
                for _, p := range plugins {
31✔
82
                        key := plugin.KeyFor(p)
16✔
83
                        if _, isConflicting := c.plugins[key]; isConflicting {
18✔
84
                                return fmt.Errorf("two plugins have the same key: %q", key)
2✔
85
                        }
2✔
86
                        if err := plugin.Validate(p); err != nil {
18✔
87
                                return fmt.Errorf("broken pre-set plugin %q: %w", key, err)
4✔
88
                        }
4✔
89
                        c.plugins[key] = p
10✔
90
                }
91
                return nil
9✔
92
        }
93
}
94

95
// WithDefaultPlugins is an Option that sets the CLI's default plugins.
96
//
97
// Specifying any invalid plugin results in an error.
98
func WithDefaultPlugins(projectVersion config.Version, plugins ...plugin.Plugin) Option {
15✔
99
        return func(c *CLI) error {
30✔
100
                if err := projectVersion.Validate(); err != nil {
16✔
101
                        return fmt.Errorf("broken pre-set project version %q for default plugins: %w", projectVersion, err)
1✔
102
                }
1✔
103
                if len(plugins) == 0 {
15✔
104
                        return fmt.Errorf("empty set of plugins provided for project version %q", projectVersion)
1✔
105
                }
1✔
106
                for _, p := range plugins {
26✔
107
                        if err := plugin.Validate(p); err != nil {
17✔
108
                                return fmt.Errorf("broken pre-set default plugin %q: %w", plugin.KeyFor(p), err)
4✔
109
                        }
4✔
110
                        if !plugin.SupportsVersion(p, projectVersion) {
10✔
111
                                return fmt.Errorf("default plugin %q doesn't support version %q", plugin.KeyFor(p), projectVersion)
1✔
112
                        }
1✔
113
                        c.defaultPlugins[projectVersion] = append(c.defaultPlugins[projectVersion], plugin.KeyFor(p))
8✔
114
                }
115
                return nil
8✔
116
        }
117
}
118

119
// WithDefaultProjectVersion is an Option that sets the CLI's default project version.
120
//
121
// Setting an invalid version results in an error.
122
func WithDefaultProjectVersion(version config.Version) Option {
7✔
123
        return func(c *CLI) error {
14✔
124
                if err := version.Validate(); err != nil {
10✔
125
                        return fmt.Errorf("broken pre-set default project version %q: %w", version, err)
3✔
126
                }
3✔
127
                c.defaultProjectVersion = version
4✔
128
                return nil
4✔
129
        }
130
}
131

132
// WithExtraCommands is an Option that adds extra subcommands to the CLI.
133
//
134
// Adding extra commands that duplicate existing commands results in an error.
135
func WithExtraCommands(cmds ...*cobra.Command) Option {
3✔
136
        return func(c *CLI) error {
6✔
137
                // We don't know the commands defined by the CLI yet so we are not checking if the extra commands
3✔
138
                // conflict with a pre-existing one yet. We do this after creating the base commands.
3✔
139
                c.extraCommands = append(c.extraCommands, cmds...)
3✔
140
                return nil
3✔
141
        }
3✔
142
}
143

144
// WithExtraAlphaCommands is an Option that adds extra alpha subcommands to the CLI.
145
//
146
// Adding extra alpha commands that duplicate existing commands results in an error.
147
func WithExtraAlphaCommands(cmds ...*cobra.Command) Option {
3✔
148
        return func(c *CLI) error {
6✔
149
                // We don't know the commands defined by the CLI yet so we are not checking if the extra alpha commands
3✔
150
                // conflict with a pre-existing one yet. We do this after creating the base commands.
3✔
151
                c.extraAlphaCommands = append(c.extraAlphaCommands, cmds...)
3✔
152
                return nil
3✔
153
        }
3✔
154
}
155

156
// WithCompletion is an Option that adds the completion subcommand.
157
func WithCompletion() Option {
2✔
158
        return func(c *CLI) error {
4✔
159
                c.completionCommand = true
2✔
160
                return nil
2✔
161
        }
2✔
162
}
163

164
// WithFilesystem is an Option that allows to set the filesystem used in the CLI.
165
func WithFilesystem(filesystem machinery.Filesystem) Option {
2✔
166
        return func(c *CLI) error {
4✔
167
                if filesystem.FS == nil {
3✔
168
                        return errors.New("invalid filesystem")
1✔
169
                }
1✔
170

171
                c.fs = filesystem
1✔
172
                return nil
1✔
173
        }
174
}
175

176
// parseExternalPluginArgs returns the program arguments.
177
func parseExternalPluginArgs() (args []string) {
4✔
178
        // Loop through os.Args and only get flags and their values that should be passed to the plugins
4✔
179
        // this also removes the --plugins flag and its values from the list passed to the external plugin
4✔
180
        for i := range os.Args {
32✔
181
                if strings.Contains(os.Args[i], "--") && !strings.Contains(os.Args[i], "--plugins") {
32✔
182
                        args = append(args, os.Args[i])
4✔
183

4✔
184
                        // Don't go out of bounds and don't append the next value if it is a flag
4✔
185
                        if i+1 < len(os.Args) && !strings.Contains(os.Args[i+1], "--") {
6✔
186
                                args = append(args, os.Args[i+1])
2✔
187
                        }
2✔
188
                }
189
        }
190

191
        return args
4✔
192
}
193

194
// isHostSupported checks whether the host system is supported or not.
195
func isHostSupported(host string) bool {
39✔
196
        for _, platform := range supportedPlatforms {
112✔
197
                if host == platform {
108✔
198
                        return true
35✔
199
                }
35✔
200
        }
201
        return false
4✔
202
}
203

204
// getPluginsRoot gets the plugin root path.
205
func getPluginsRoot(host string) (pluginsRoot string, err error) {
39✔
206
        if !isHostSupported(host) {
43✔
207
                // freebsd, openbsd, windows...
4✔
208
                return "", fmt.Errorf("host not supported: %v", host)
4✔
209
        }
4✔
210

211
        // if user provides specific path, return
212
        if pluginsPath := os.Getenv("EXTERNAL_PLUGINS_PATH"); pluginsPath != "" {
39✔
213
                // verify if the path actually exists
4✔
214
                if _, err = os.Stat(pluginsPath); err != nil {
6✔
215
                        if os.IsNotExist(err) {
4✔
216
                                // the path does not exist
2✔
217
                                return "", fmt.Errorf("the specified path %s does not exist", pluginsPath)
2✔
218
                        }
2✔
219
                        // some other error
220
                        return "", fmt.Errorf("error checking the path: %w", err)
×
221
                }
222
                // the path exists
223
                return pluginsPath, nil
2✔
224
        }
225

226
        // if no specific path, detects the host system and gets the plugins root based on the host.
227
        pluginsRelativePath := filepath.Join("kubebuilder", "plugins")
31✔
228
        if xdgHome := os.Getenv("XDG_CONFIG_HOME"); xdgHome != "" {
55✔
229
                return filepath.Join(xdgHome, pluginsRelativePath), nil
24✔
230
        }
24✔
231

232
        switch host {
7✔
233
        case "darwin":
2✔
234
                slog.Debug("Detected host is macOS.")
2✔
235
                pluginsRoot = filepath.Join("Library", "Application Support", pluginsRelativePath)
2✔
236
        case "linux":
5✔
237
                slog.Debug("Detected host is Linux.")
5✔
238
                pluginsRoot = filepath.Join(".config", pluginsRelativePath)
5✔
239
        }
240

241
        userHomeDir, err := os.UserHomeDir()
7✔
242
        if err != nil {
8✔
243
                return "", fmt.Errorf("error retrieving home dir: %w", err)
1✔
244
        }
1✔
245

246
        return filepath.Join(userHomeDir, pluginsRoot), nil
6✔
247
}
248

249
// DiscoverExternalPlugins discovers the external plugins in the plugins root directory
250
// and adds them to external.Plugin.
251
func DiscoverExternalPlugins(filesystem afero.Fs) (ps []plugin.Plugin, err error) {
7✔
252
        pluginsRoot, err := retrievePluginsRoot(runtime.GOOS)
7✔
253
        if err != nil {
8✔
254
                slog.Error("could not get plugins root", "error", err)
1✔
255
                return nil, fmt.Errorf("could not get plugins root: %w", err)
1✔
256
        }
1✔
257

258
        rootInfo, err := filesystem.Stat(pluginsRoot)
6✔
259
        if err != nil {
7✔
260
                if errors.Is(err, afero.ErrFileNotFound) {
2✔
261
                        slog.Debug("External plugins dir does not exist, skipping external plugin parsing", "dir", pluginsRoot)
1✔
262
                        return nil, nil
1✔
263
                }
1✔
264
                return nil, fmt.Errorf("error getting stats for plugins %s: %w", pluginsRoot, err)
×
265
        }
266
        if !rootInfo.IsDir() {
5✔
NEW
267
                slog.Debug("External plugins path is not a directory, skipping external plugin parsing", "path", pluginsRoot)
×
268
                return nil, nil
×
269
        }
×
270

271
        pluginInfos, err := afero.ReadDir(filesystem, pluginsRoot)
5✔
272
        if err != nil {
5✔
273
                return nil, fmt.Errorf("error reading plugins directory %q: %w", pluginsRoot, err)
×
274
        }
×
275

276
        for _, pluginInfo := range pluginInfos {
11✔
277
                if !pluginInfo.IsDir() {
6✔
NEW
278
                        slog.Debug("skipping parsing, not a directory", "name", pluginInfo.Name())
×
279
                        continue
×
280
                }
281

282
                versions, err := afero.ReadDir(filesystem, filepath.Join(pluginsRoot, pluginInfo.Name()))
6✔
283
                if err != nil {
6✔
284
                        return nil, fmt.Errorf("error reading plugin directory %s: %w",
×
285
                                filepath.Join(pluginsRoot, pluginInfo.Name()), err)
×
286
                }
×
287

288
                for _, version := range versions {
12✔
289
                        if !version.IsDir() {
6✔
NEW
290
                                slog.Debug("skipping parsing, not a directory", "name", version.Name())
×
291
                                continue
×
292
                        }
293

294
                        pluginFiles, err := afero.ReadDir(filesystem, filepath.Join(pluginsRoot, pluginInfo.Name(), version.Name()))
6✔
295
                        if err != nil {
6✔
296
                                return nil, fmt.Errorf("error reading plugion version directory %q: %w",
×
297
                                        filepath.Join(pluginsRoot, pluginInfo.Name(), version.Name()), err)
×
298
                        }
×
299

300
                        for _, pluginFile := range pluginFiles {
12✔
301
                                // find the executable that matches the same name as info.Name().
6✔
302
                                // if no match is found, compare the external plugin string name before dot
6✔
303
                                // and match it with info.Name() which is the external plugin root dir.
6✔
304
                                // for example: sample.sh --> sample, externalplugin.py --> externalplugin
6✔
305
                                trimmedPluginName := strings.Split(pluginFile.Name(), ".")
6✔
306
                                if trimmedPluginName[0] == "" {
7✔
307
                                        return nil, fmt.Errorf("invalid plugin name found %q", pluginFile.Name())
1✔
308
                                }
1✔
309

310
                                if pluginFile.Name() == pluginInfo.Name() || trimmedPluginName[0] == pluginInfo.Name() {
9✔
311
                                        // check whether the external plugin is an executable.
4✔
312
                                        if !isPluginExecutable(pluginFile.Mode()) {
5✔
313
                                                return nil, fmt.Errorf("external plugin %q found in path is not an executable", pluginFile.Name())
1✔
314
                                        }
1✔
315

316
                                        ep := external.Plugin{
3✔
317
                                                PName:                     pluginInfo.Name(),
3✔
318
                                                Path:                      filepath.Join(pluginsRoot, pluginInfo.Name(), version.Name(), pluginFile.Name()),
3✔
319
                                                PSupportedProjectVersions: []config.Version{cfgv3.Version},
3✔
320
                                                Args:                      parseExternalPluginArgs(),
3✔
321
                                        }
3✔
322

3✔
323
                                        if err = ep.PVersion.Parse(version.Name()); err != nil {
3✔
324
                                                return nil, fmt.Errorf("error parsing external plugin version %q: %w", version.Name(), err)
×
325
                                        }
×
326

327
                                        slog.Info("Adding external plugin", "plugin name", ep.Name())
3✔
328

3✔
329
                                        ps = append(ps, ep)
3✔
330
                                }
331
                        }
332
                }
333
        }
334

335
        return ps, nil
3✔
336
}
337

338
// isPluginExecutable checks if a plugin is an executable based on the bitmask and returns true or false.
339
func isPluginExecutable(mode fs.FileMode) bool {
4✔
340
        return mode&0o111 != 0
4✔
341
}
4✔
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