Skip to content

Commit 265658e

Browse files
authored
Merge pull request #2338 from rashmigottipati/plugin-phase-2
✨Plugin phase 2 implementation
2 parents 4c4e884 + 2fe045d commit 265658e

File tree

11 files changed

+1061
-0
lines changed

11 files changed

+1061
-0
lines changed

cmd/main.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ package main
1919
import (
2020
"log"
2121

22+
"github.com/sirupsen/logrus"
23+
"github.com/spf13/afero"
2224
"sigs.k8s.io/kubebuilder/v3/pkg/cli"
2325
cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
2426
cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3"
27+
"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
2528
"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
2629
kustomizecommonv1 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/common/kustomize/v1"
2730
"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
@@ -38,6 +41,14 @@ func main() {
3841
golangv3.Plugin{},
3942
)
4043

44+
fs := machinery.Filesystem{
45+
FS: afero.NewOsFs(),
46+
}
47+
externalPlugins, err := cli.DiscoverExternalPlugins(fs.FS)
48+
if err != nil {
49+
logrus.Error(err)
50+
}
51+
4152
c, err := cli.New(
4253
cli.WithCommandName("kubebuilder"),
4354
cli.WithVersion(versionString()),
@@ -48,6 +59,7 @@ func main() {
4859
&kustomizecommonv1.Plugin{},
4960
&declarativev1.Plugin{},
5061
),
62+
cli.WithPlugins(externalPlugins...),
5163
cli.WithDefaultPlugins(cfgv2.Version, golangv2.Plugin{}),
5264
cli.WithDefaultPlugins(cfgv3.Version, gov3Bundle),
5365
cli.WithDefaultProjectVersion(cfgv3.Version),

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
476476
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
477477
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
478478
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
479+
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
479480
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
480481
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
481482
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=

pkg/cli/options.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,27 @@ limitations under the License.
1717
package cli
1818

1919
import (
20+
"errors"
2021
"fmt"
22+
"io/fs"
23+
"os"
24+
"path/filepath"
25+
"runtime"
26+
"strings"
2127

28+
"github.com/sirupsen/logrus"
29+
"github.com/spf13/afero"
2230
"github.com/spf13/cobra"
2331

2432
"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"
2535
"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
36+
"sigs.k8s.io/kubebuilder/v3/pkg/plugins/external"
2637
)
2738

39+
var retrievePluginsRoot = getPluginsRoot
40+
2841
// Option is a function used as arguments to New in order to configure the resulting CLI.
2942
type Option func(*CLI) error
3043

@@ -139,3 +152,140 @@ func WithCompletion() Option {
139152
return nil
140153
}
141154
}
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

Comments
 (0)