@@ -17,14 +17,27 @@ limitations under the License.
17
17
package cli
18
18
19
19
import (
20
+ "errors"
20
21
"fmt"
22
+ "io/fs"
23
+ "os"
24
+ "path/filepath"
25
+ "runtime"
26
+ "strings"
21
27
28
+ "github.com/sirupsen/logrus"
29
+ "github.com/spf13/afero"
22
30
"github.com/spf13/cobra"
23
31
24
32
"sigs.k8s.io/kubebuilder/v3/pkg/config"
33
+ cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
34
+ cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3"
25
35
"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
36
+ "sigs.k8s.io/kubebuilder/v3/pkg/plugins/external"
26
37
)
27
38
39
+ var retrievePluginsRoot = getPluginsRoot
40
+
28
41
// Option is a function used as arguments to New in order to configure the resulting CLI.
29
42
type Option func (* CLI ) error
30
43
@@ -139,3 +152,140 @@ func WithCompletion() Option {
139
152
return nil
140
153
}
141
154
}
155
+
156
+ // parseExternalPluginArgs returns the program arguments.
157
+ func parseExternalPluginArgs () (args []string ) {
158
+ args = make ([]string , len (os .Args )- 1 )
159
+ copy (args , os .Args [1 :])
160
+
161
+ return args
162
+ }
163
+
164
+ // getPluginsRoot detects the host system and gets the plugins root based on the host.
165
+ func getPluginsRoot (host string ) (pluginsRoot string , err error ) {
166
+ switch host {
167
+ case "darwin" :
168
+ logrus .Debugf ("Detected host is macOS." )
169
+ pluginsRoot = filepath .Join ("Library" , "ApplicationSupport" , "kubebuilder" , "plugins" )
170
+ case "linux" :
171
+ logrus .Debugf ("Detected host is Linux." )
172
+ pluginsRoot = filepath .Join (".config" , "kubebuilder" , "plugins" )
173
+ default :
174
+ // freebsd, openbsd, windows...
175
+ return "" , fmt .Errorf ("Host not supported: %v" , host )
176
+ }
177
+ userHomeDir , err := getHomeDir ()
178
+ if err != nil {
179
+ return "" , fmt .Errorf ("error retrieving home dir: %v" , err )
180
+ }
181
+ pluginsRoot = filepath .Join (userHomeDir , pluginsRoot )
182
+
183
+ return pluginsRoot , nil
184
+ }
185
+
186
+ // DiscoverExternalPlugins discovers the external plugins in the plugins root directory
187
+ // and adds them to external.Plugin.
188
+ func DiscoverExternalPlugins (fs afero.Fs ) (ps []plugin.Plugin , err error ) {
189
+ pluginsRoot , err := retrievePluginsRoot (runtime .GOOS )
190
+ if err != nil {
191
+ logrus .Errorf ("could not get plugins root: %v" , err )
192
+ return nil , err
193
+ }
194
+
195
+ rootInfo , err := fs .Stat (pluginsRoot )
196
+ if err != nil {
197
+ if errors .Is (err , afero .ErrFileNotFound ) {
198
+ logrus .Debugf ("External plugins dir %q does not exist, skipping external plugin parsing" , pluginsRoot )
199
+ return nil , nil
200
+ }
201
+ return nil , err
202
+ }
203
+ if ! rootInfo .IsDir () {
204
+ logrus .Debugf ("External plugins path %q is not a directory, skipping external plugin parsing" , pluginsRoot )
205
+ return nil , nil
206
+ }
207
+
208
+ pluginInfos , err := afero .ReadDir (fs , pluginsRoot )
209
+ if err != nil {
210
+ return nil , err
211
+ }
212
+
213
+ for _ , pluginInfo := range pluginInfos {
214
+ if ! pluginInfo .IsDir () {
215
+ logrus .Debugf ("%q is not a directory so skipping parsing" , pluginInfo .Name ())
216
+ continue
217
+ }
218
+
219
+ versions , err := afero .ReadDir (fs , filepath .Join (pluginsRoot , pluginInfo .Name ()))
220
+ if err != nil {
221
+ return nil , err
222
+ }
223
+
224
+ for _ , version := range versions {
225
+ if ! version .IsDir () {
226
+ logrus .Debugf ("%q is not a directory so skipping parsing" , version .Name ())
227
+ continue
228
+ }
229
+
230
+ pluginFiles , err := afero .ReadDir (fs , filepath .Join (pluginsRoot , pluginInfo .Name (), version .Name ()))
231
+ if err != nil {
232
+ return nil , err
233
+ }
234
+
235
+ for _ , pluginFile := range pluginFiles {
236
+ // find the executable that matches the same name as info.Name().
237
+ // if no match is found, compare the external plugin string name before dot
238
+ // and match it with info.Name() which is the external plugin root dir.
239
+ // for example: sample.sh --> sample, externalplugin.py --> externalplugin
240
+ trimmedPluginName := strings .Split (pluginFile .Name (), "." )
241
+ if trimmedPluginName [0 ] == "" {
242
+ return nil , fmt .Errorf ("Invalid plugin name found %q" , pluginFile .Name ())
243
+ }
244
+
245
+ if pluginFile .Name () == pluginInfo .Name () || trimmedPluginName [0 ] == pluginInfo .Name () {
246
+ // check whether the external plugin is an executable.
247
+ if ! isPluginExectuable (pluginFile .Mode ()) {
248
+ return nil , fmt .Errorf ("External plugin %q found in path is not an executable" , pluginFile .Name ())
249
+ }
250
+
251
+ ep := external.Plugin {
252
+ PName : pluginInfo .Name (),
253
+ Path : filepath .Join (pluginsRoot , pluginInfo .Name (), version .Name (), pluginFile .Name ()),
254
+ PSupportedProjectVersions : []config.Version {cfgv2 .Version , cfgv3 .Version },
255
+ Args : parseExternalPluginArgs (),
256
+ }
257
+
258
+ if err := ep .PVersion .Parse (version .Name ()); err != nil {
259
+ return nil , err
260
+ }
261
+
262
+ logrus .Printf ("Adding external plugin: %s" , ep .Name ())
263
+
264
+ ps = append (ps , ep )
265
+
266
+ }
267
+ }
268
+ }
269
+
270
+ }
271
+
272
+ return ps , nil
273
+ }
274
+
275
+ // isPluginExectuable checks if a plugin is an executable based on the bitmask and returns true or false.
276
+ func isPluginExectuable (mode fs.FileMode ) bool {
277
+ return mode & 0111 != 0
278
+ }
279
+
280
+ // getHomeDir returns $XDG_CONFIG_HOME if set, otherwise $HOME.
281
+ func getHomeDir () (string , error ) {
282
+ var err error
283
+ xdgHome := os .Getenv ("XDG_CONFIG_HOME" )
284
+ if xdgHome == "" {
285
+ xdgHome , err = os .UserHomeDir ()
286
+ if err != nil {
287
+ return "" , err
288
+ }
289
+ }
290
+ return xdgHome , nil
291
+ }
0 commit comments