-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
options.go
333 lines (286 loc) · 10.3 KB
/
options.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cli
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/sirupsen/logrus"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"sigs.k8s.io/kubebuilder/v4/pkg/config"
cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
"sigs.k8s.io/kubebuilder/v4/pkg/plugins/external"
)
var retrievePluginsRoot = getPluginsRoot
// Option is a function used as arguments to New in order to configure the resulting CLI.
type Option func(*CLI) error
// WithCommandName is an Option that sets the CLI's root command name.
func WithCommandName(name string) Option {
return func(c *CLI) error {
c.commandName = name
return nil
}
}
// WithVersion is an Option that defines the version string of the CLI.
func WithVersion(version string) Option {
return func(c *CLI) error {
c.version = version
return nil
}
}
// WithDescription is an Option that sets the CLI's root description.
func WithDescription(description string) Option {
return func(c *CLI) error {
c.description = description
return nil
}
}
// WithPlugins is an Option that sets the CLI's plugins.
//
// Specifying any invalid plugin results in an error.
func WithPlugins(plugins ...plugin.Plugin) Option {
return func(c *CLI) error {
for _, p := range plugins {
key := plugin.KeyFor(p)
if _, isConflicting := c.plugins[key]; isConflicting {
return fmt.Errorf("two plugins have the same key: %q", key)
}
if err := plugin.Validate(p); err != nil {
return fmt.Errorf("broken pre-set plugin %q: %v", key, err)
}
c.plugins[key] = p
}
return nil
}
}
// WithDefaultPlugins is an Option that sets the CLI's default plugins.
//
// Specifying any invalid plugin results in an error.
func WithDefaultPlugins(projectVersion config.Version, plugins ...plugin.Plugin) Option {
return func(c *CLI) error {
if err := projectVersion.Validate(); err != nil {
return fmt.Errorf("broken pre-set project version %q for default plugins: %w", projectVersion, err)
}
if len(plugins) == 0 {
return fmt.Errorf("empty set of plugins provided for project version %q", projectVersion)
}
for _, p := range plugins {
if err := plugin.Validate(p); err != nil {
return fmt.Errorf("broken pre-set default plugin %q: %v", plugin.KeyFor(p), err)
}
if !plugin.SupportsVersion(p, projectVersion) {
return fmt.Errorf("default plugin %q doesn't support version %q", plugin.KeyFor(p), projectVersion)
}
c.defaultPlugins[projectVersion] = append(c.defaultPlugins[projectVersion], plugin.KeyFor(p))
}
return nil
}
}
// WithDefaultProjectVersion is an Option that sets the CLI's default project version.
//
// Setting an invalid version results in an error.
func WithDefaultProjectVersion(version config.Version) Option {
return func(c *CLI) error {
if err := version.Validate(); err != nil {
return fmt.Errorf("broken pre-set default project version %q: %v", version, err)
}
c.defaultProjectVersion = version
return nil
}
}
// WithExtraCommands is an Option that adds extra subcommands to the CLI.
//
// Adding extra commands that duplicate existing commands results in an error.
func WithExtraCommands(cmds ...*cobra.Command) Option {
return func(c *CLI) error {
// We don't know the commands defined by the CLI yet so we are not checking if the extra commands
// conflict with a pre-existing one yet. We do this after creating the base commands.
c.extraCommands = append(c.extraCommands, cmds...)
return nil
}
}
// WithExtraAlphaCommands is an Option that adds extra alpha subcommands to the CLI.
//
// Adding extra alpha commands that duplicate existing commands results in an error.
func WithExtraAlphaCommands(cmds ...*cobra.Command) Option {
return func(c *CLI) error {
// We don't know the commands defined by the CLI yet so we are not checking if the extra alpha commands
// conflict with a pre-existing one yet. We do this after creating the base commands.
c.extraAlphaCommands = append(c.extraAlphaCommands, cmds...)
return nil
}
}
// WithCompletion is an Option that adds the completion subcommand.
func WithCompletion() Option {
return func(c *CLI) error {
c.completionCommand = true
return nil
}
}
// WithFilesystem is an Option that allows to set the filesystem used in the CLI.
func WithFilesystem(fs machinery.Filesystem) Option {
return func(c *CLI) error {
if fs.FS == nil {
return errors.New("invalid filesystem")
}
c.fs = fs
return nil
}
}
// parseExternalPluginArgs returns the program arguments.
func parseExternalPluginArgs() (args []string) {
// Loop through os.Args and only get flags and their values that should be passed to the plugins
// this also removes the --plugins flag and its values from the list passed to the external plugin
for i := range os.Args {
if strings.Contains(os.Args[i], "--") && !strings.Contains(os.Args[i], "--plugins") {
args = append(args, os.Args[i])
// Don't go out of bounds and don't append the next value if it is a flag
if i+1 < len(os.Args) && !strings.Contains(os.Args[i+1], "--") {
args = append(args, os.Args[i+1])
}
}
}
return args
}
// isHostSupported checks whether the host system is supported or not.
func isHostSupported(host string) bool {
for _, platform := range supportedPlatforms {
if host == platform {
return true
}
}
return false
}
// getPluginsRoot gets the plugin root path.
func getPluginsRoot(host string) (pluginsRoot string, err error) {
if !isHostSupported(host) {
// freebsd, openbsd, windows...
return "", fmt.Errorf("host not supported: %v", host)
}
// if user provides specific path, return
if pluginsPath := os.Getenv("EXTERNAL_PLUGINS_PATH"); pluginsPath != "" {
// verify if the path actually exists
if _, err := os.Stat(pluginsPath); err != nil {
if os.IsNotExist(err) {
// the path does not exist
return "", fmt.Errorf("the specified path %s does not exist", pluginsPath)
}
// some other error
return "", fmt.Errorf("error checking the path: %v", err)
}
// the path exists
return pluginsPath, nil
}
// if no specific path, detects the host system and gets the plugins root based on the host.
pluginsRelativePath := filepath.Join("kubebuilder", "plugins")
if xdgHome := os.Getenv("XDG_CONFIG_HOME"); xdgHome != "" {
return filepath.Join(xdgHome, pluginsRelativePath), nil
}
switch host {
case "darwin":
logrus.Debugf("Detected host is macOS.")
pluginsRoot = filepath.Join("Library", "Application Support", pluginsRelativePath)
case "linux":
logrus.Debugf("Detected host is Linux.")
pluginsRoot = filepath.Join(".config", pluginsRelativePath)
}
userHomeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("error retrieving home dir: %v", err)
}
return filepath.Join(userHomeDir, pluginsRoot), nil
}
// DiscoverExternalPlugins discovers the external plugins in the plugins root directory
// and adds them to external.Plugin.
func DiscoverExternalPlugins(fs afero.Fs) (ps []plugin.Plugin, err error) {
pluginsRoot, err := retrievePluginsRoot(runtime.GOOS)
if err != nil {
logrus.Errorf("could not get plugins root: %v", err)
return nil, err
}
rootInfo, err := fs.Stat(pluginsRoot)
if err != nil {
if errors.Is(err, afero.ErrFileNotFound) {
logrus.Debugf("External plugins dir %q does not exist, skipping external plugin parsing", pluginsRoot)
return nil, nil
}
return nil, err
}
if !rootInfo.IsDir() {
logrus.Debugf("External plugins path %q is not a directory, skipping external plugin parsing", pluginsRoot)
return nil, nil
}
pluginInfos, err := afero.ReadDir(fs, pluginsRoot)
if err != nil {
return nil, err
}
for _, pluginInfo := range pluginInfos {
if !pluginInfo.IsDir() {
logrus.Debugf("%q is not a directory so skipping parsing", pluginInfo.Name())
continue
}
versions, err := afero.ReadDir(fs, filepath.Join(pluginsRoot, pluginInfo.Name()))
if err != nil {
return nil, err
}
for _, version := range versions {
if !version.IsDir() {
logrus.Debugf("%q is not a directory so skipping parsing", version.Name())
continue
}
pluginFiles, err := afero.ReadDir(fs, filepath.Join(pluginsRoot, pluginInfo.Name(), version.Name()))
if err != nil {
return nil, err
}
for _, pluginFile := range pluginFiles {
// find the executable that matches the same name as info.Name().
// if no match is found, compare the external plugin string name before dot
// and match it with info.Name() which is the external plugin root dir.
// for example: sample.sh --> sample, externalplugin.py --> externalplugin
trimmedPluginName := strings.Split(pluginFile.Name(), ".")
if trimmedPluginName[0] == "" {
return nil, fmt.Errorf("Invalid plugin name found %q", pluginFile.Name())
}
if pluginFile.Name() == pluginInfo.Name() || trimmedPluginName[0] == pluginInfo.Name() {
// check whether the external plugin is an executable.
if !isPluginExectuable(pluginFile.Mode()) {
return nil, fmt.Errorf("External plugin %q found in path is not an executable", pluginFile.Name())
}
ep := external.Plugin{
PName: pluginInfo.Name(),
Path: filepath.Join(pluginsRoot, pluginInfo.Name(), version.Name(), pluginFile.Name()),
PSupportedProjectVersions: []config.Version{cfgv3.Version},
Args: parseExternalPluginArgs(),
}
if err := ep.PVersion.Parse(version.Name()); err != nil {
return nil, err
}
logrus.Printf("Adding external plugin: %s", ep.Name())
ps = append(ps, ep)
}
}
}
}
return ps, nil
}
// isPluginExectuable checks if a plugin is an executable based on the bitmask and returns true or false.
func isPluginExectuable(mode fs.FileMode) bool {
return mode&0111 != 0
}