Skip to content

Commit

Permalink
(Plugin phase 2): Add implementation to discover and run Phase 2 Plugins
Browse files Browse the repository at this point in the history
Add discovery of external plugins

Define external plugin interface

Add subcommands functionality for init, create api, createa webhook, edit

Add unit tests for Discovery and Scaffold functions

Signed-off-by: Rashmi Gottipati <[email protected]>
  • Loading branch information
rashmigottipati committed May 13, 2022
1 parent 8e47721 commit 2fe045d
Show file tree
Hide file tree
Showing 11 changed files with 1,061 additions and 0 deletions.
12 changes: 12 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ package main
import (
"log"

"github.com/sirupsen/logrus"
"github.com/spf13/afero"
"sigs.k8s.io/kubebuilder/v3/pkg/cli"
cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3"
"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
kustomizecommonv1 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/common/kustomize/v1"
"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
Expand All @@ -38,6 +41,14 @@ func main() {
golangv3.Plugin{},
)

fs := machinery.Filesystem{
FS: afero.NewOsFs(),
}
externalPlugins, err := cli.DiscoverExternalPlugins(fs.FS)
if err != nil {
logrus.Error(err)
}

c, err := cli.New(
cli.WithCommandName("kubebuilder"),
cli.WithVersion(versionString()),
Expand All @@ -48,6 +59,7 @@ func main() {
&kustomizecommonv1.Plugin{},
&declarativev1.Plugin{},
),
cli.WithPlugins(externalPlugins...),
cli.WithDefaultPlugins(cfgv2.Version, golangv2.Plugin{}),
cli.WithDefaultPlugins(cfgv3.Version, gov3Bundle),
cli.WithDefaultProjectVersion(cfgv3.Version),
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
Expand Down
150 changes: 150 additions & 0 deletions pkg/cli/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,27 @@ 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/v3/pkg/config"
cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3"
"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
"sigs.k8s.io/kubebuilder/v3/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

Expand Down Expand Up @@ -139,3 +152,140 @@ func WithCompletion() Option {
return nil
}
}

// parseExternalPluginArgs returns the program arguments.
func parseExternalPluginArgs() (args []string) {
args = make([]string, len(os.Args)-1)
copy(args, os.Args[1:])

return args
}

// getPluginsRoot detects the host system and gets the plugins root based on the host.
func getPluginsRoot(host string) (pluginsRoot string, err error) {
switch host {
case "darwin":
logrus.Debugf("Detected host is macOS.")
pluginsRoot = filepath.Join("Library", "ApplicationSupport", "kubebuilder", "plugins")
case "linux":
logrus.Debugf("Detected host is Linux.")
pluginsRoot = filepath.Join(".config", "kubebuilder", "plugins")
default:
// freebsd, openbsd, windows...
return "", fmt.Errorf("Host not supported: %v", host)
}
userHomeDir, err := getHomeDir()
if err != nil {
return "", fmt.Errorf("error retrieving home dir: %v", err)
}
pluginsRoot = filepath.Join(userHomeDir, pluginsRoot)

return 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{cfgv2.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
}

// getHomeDir returns $XDG_CONFIG_HOME if set, otherwise $HOME.
func getHomeDir() (string, error) {
var err error
xdgHome := os.Getenv("XDG_CONFIG_HOME")
if xdgHome == "" {
xdgHome, err = os.UserHomeDir()
if err != nil {
return "", err
}
}
return xdgHome, nil
}
Loading

0 comments on commit 2fe045d

Please sign in to comment.