Skip to content

Commit

Permalink
Add support for internal plugins (knative#902)
Browse files Browse the repository at this point in the history
* chore: Add support for internal plugins

* added test for internal plugin lookup

* add changelog entry

* fix formatting

* update to latest changes on main

* Added more tests + some docs

* formatting fix

* moved to proper injection label for setting up the broker in integration test
  • Loading branch information
rhuss committed Sep 9, 2020
1 parent e4b2a1b commit 627704e
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 21 deletions.
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]

| 🎁
| 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
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
}
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)
}
}
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
}
}
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

0 comments on commit 627704e

Please sign in to comment.