Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for internal plugins #902

Merged
merged 8 commits into from
Aug 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
| Add sugar controller to E2E tests
| https://github.com/knative/client/pull/920[#920]

| 🎁
| Add support for internal plugins
| https://github.com/knative/client/pull/880[#880]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

902, can be fixed in update to changelog PR for release as well.


| 🎁
| Add "url" output format to return service url in service describe
| https://github.com/knative/client/pull/916[#916]
Expand Down
36 changes: 36 additions & 0 deletions docs/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,39 @@ Please refer to the documentation and examples for more information on how to
write your own plugins.

- [kn plugin](../cmd/kn_plugin.md) - Plugin command group


## Plugin Inlining

It is possible to inline plugins that are written in golang.
The following steps are required:

* In your plugin project, create a implementation of the `Plugin` interface and add it to the global `plugin.InternalPlugins` slice in your `init()` method, like in this example:

```go
package plugin

import (
"knative.dev/client/pkg/kn/plugin"
)

func init() {
plugin.InternalPlugins = append(plugin.InternalPlugins, &myPlugin{})
}
```

* In your fork of the kn client, add a file `plugin_register.go` to the root package directory which imports your plugin's implementation package:

```go
package root

import (
_ "github.com/rhuss/myplugin/plugin"
)

// RegisterInlinePlugins is an empty function which however forces the
// compiler to run all init() methods of the registered imports
func RegisterInlinePlugins() {}
```

* Update you `go.mod` file with the new dependency and build your custom distribution of `kn`
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
Expand Down Expand Up @@ -809,6 +810,7 @@ github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qo
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
Expand All @@ -818,6 +820,7 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
Expand Down
4 changes: 2 additions & 2 deletions lib/test/broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (

// LabelNamespaceForDefaultBroker adds label 'knative-eventing-injection=enabled' to the configured namespace
func LabelNamespaceForDefaultBroker(r *KnRunResultCollector) error {
cmd := []string{"label", "namespace", r.KnTest().Kn().Namespace(), v1beta1.DeprecatedInjectionAnnotation + "=enabled"}
cmd := []string{"label", "namespace", r.KnTest().Kn().Namespace(), v1beta1.InjectionAnnotation + "=enabled"}
_, err := Kubectl{}.Run(cmd...)

if err != nil {
Expand All @@ -43,7 +43,7 @@ func LabelNamespaceForDefaultBroker(r *KnRunResultCollector) error {

// UnlabelNamespaceForDefaultBroker removes label 'knative-eventing-injection=enabled' from the configured namespace
func UnlabelNamespaceForDefaultBroker(r *KnRunResultCollector) {
cmd := []string{"label", "namespace", r.KnTest().Kn().Namespace(), v1beta1.DeprecatedInjectionAnnotation + "-"}
cmd := []string{"label", "namespace", r.KnTest().Kn().Namespace(), v1beta1.InjectionAnnotation + "-"}
_, err := Kubectl{}.Run(cmd...)
if err != nil {
r.T().Fatalf("error executing '%s': %s", strings.Join(cmd, " "), err.Error())
Expand Down
2 changes: 1 addition & 1 deletion pkg/kn/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func BootstrapConfig() error {
viper.SetConfigFile(GlobalConfig.ConfigFile())
viper.AutomaticEnv() // read in environment variables that match

// Defaults are taken from the parsed flags, which in turn have bootstrapDefaults
// Defaults are taken from the parsed flags, which in turn have bootstrap defaults
// TODO: Re-enable when legacy handling for plugin config has been removed
// For now default handling is happening directly in the getter of GlobalConfig
// viper.SetDefault(keyPluginsDirectory, bootstrapDefaults.pluginsDir)
Expand Down
84 changes: 71 additions & 13 deletions pkg/kn/plugin/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import (
"github.com/spf13/cobra"
)

// Allow plugins to register to this slice for inlining
var InternalPlugins PluginList

// Interface describing a plugin
type Plugin interface {
// Get the name of the plugin (the file name without extensions)
Expand Down Expand Up @@ -99,13 +102,19 @@ func (manager *Manager) FindPlugin(parts []string) (Plugin, error) {
return nil, nil
}

// Try to find internal plugin fist
rhuss marked this conversation as resolved.
Show resolved Hide resolved
plugin := lookupInternalPlugin(parts)
if plugin != nil {
return plugin, nil
}

// Try to find plugin in pluginsDir
pluginDir, err := homedir.Expand(manager.pluginsDir)
if err != nil {
return nil, err
}

return findMostSpecificPlugin(pluginDir, parts, manager.lookupInPath)
return findMostSpecificPluginInPath(pluginDir, parts, manager.lookupInPath)
}

// ListPlugins lists all plugins that can be found in the plugin directory or in the path (if configured)
Expand All @@ -116,7 +125,9 @@ func (manager *Manager) ListPlugins() (PluginList, error) {
// ListPluginsForCommandGroup lists all plugins that can be found in the plugin directory or in the path (if configured),
// and which fits to a command group
func (manager *Manager) ListPluginsForCommandGroup(commandGroupParts []string) (PluginList, error) {
var plugins []Plugin

// Initialize with list of internal plugins
var plugins = append([]Plugin{}, filterPluginsByCommandGroup(InternalPlugins, commandGroupParts)...)

dirs, err := manager.pluginLookupDirectories()
if err != nil {
Expand All @@ -125,6 +136,9 @@ func (manager *Manager) ListPluginsForCommandGroup(commandGroupParts []string) (

// Examine all files in possible plugin directories
hasSeen := make(map[string]bool)
for _, pl := range plugins {
hasSeen[pl.Name()] = true
rhuss marked this conversation as resolved.
Show resolved Hide resolved
}
for _, dir := range dirs {
files, err := ioutil.ReadDir(dir)

Expand All @@ -144,12 +158,12 @@ func (manager *Manager) ListPluginsForCommandGroup(commandGroupParts []string) (
}

// Check if plugin matches a command group
if !isPartOfCommandGroup(commandGroupParts, f.Name()) {
if !isPluginFileNamePartOfCommandGroup(commandGroupParts, f.Name()) {
continue
}

// Ignore all plugins that are shadowed
if _, ok := hasSeen[name]; !ok {
if seen, ok := hasSeen[name]; !ok || !seen {
plugins = append(plugins, &plugin{
path: filepath.Join(dir, f.Name()),
name: stripWindowsExecExtensions(f.Name()),
Expand All @@ -165,18 +179,34 @@ func (manager *Manager) ListPluginsForCommandGroup(commandGroupParts []string) (
return plugins, nil
}

func isPartOfCommandGroup(commandGroupParts []string, name string) bool {
func filterPluginsByCommandGroup(plugins PluginList, commandGroupParts []string) PluginList {
ret := PluginList{}
for _, pl := range plugins {
if isPartOfCommandGroup(commandGroupParts, pl.CommandParts()) {
ret = append(ret, pl)
}
}
rhuss marked this conversation as resolved.
Show resolved Hide resolved
return ret
}

func isPartOfCommandGroup(commandGroupParts []string, commandParts []string) bool {
if len(commandParts) != len(commandGroupParts)+1 {
return false
}
for i := range commandGroupParts {
if commandParts[i] != commandGroupParts[i] {
return false
}
}
rhuss marked this conversation as resolved.
Show resolved Hide resolved
return true
}

func isPluginFileNamePartOfCommandGroup(commandGroupParts []string, pluginFileName string) bool {
if commandGroupParts == nil {
return true
}

commandParts := extractPluginCommandFromFileName(name)

// commandParts must be one more element then the parts of the command group
// it belongs to. E.g. for the command "service", "log" (2 elements) the containing
// group only has one element ("service"). This condition is here for
// shortcut and ensure that we don't run in an out-of-bound array error
// in the loop below.
commandParts := extractPluginCommandFromFileName(pluginFileName)
if len(commandParts) != len(commandGroupParts)+1 {
return false
}
Expand Down Expand Up @@ -343,7 +373,7 @@ func stripWindowsExecExtensions(name string) string {
// Return the path and the parts building the most specific plugin in the given directory
// If lookupInPath is true, then also the OS PATH is checked.
// An error returned if any IO operation fails
func findMostSpecificPlugin(dir string, parts []string, lookupInPath bool) (Plugin, error) {
func findMostSpecificPluginInPath(dir string, parts []string, lookupInPath bool) (Plugin, error) {
for i := len(parts); i > 0; i-- {

// Construct plugin name to lookup
Expand Down Expand Up @@ -429,3 +459,31 @@ func findInDirOrPath(name string, dir string, lookupInPath bool) (string, error)
// Not found
return "", nil
}

// lookupInternalPlugin looks up internally registered plugins. Return nil if none is found.
// Start with longest argument path first to find the most specific match
func lookupInternalPlugin(parts []string) Plugin {
for i := len(parts); i > 0; i-- {
checkParts := parts[0:i]
for _, plugin := range InternalPlugins {
if equalsSlice(plugin.CommandParts(), checkParts) {
return plugin
}
}
}
return nil
}

// equalsSlice return true if two string slices contain the same elements
func equalsSlice(a, b []string) bool {
if len(a) != len(b) || len(a) == 0 {
return false
}

for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
Loading