Context Sharing POC (#1855)

* WIP: Context Sharing POC

* More lazy init

* Fix imports
This commit is contained in:
David Simansky 2023-10-20 16:37:21 +02:00 committed by GitHub
parent c318a2224c
commit b658574638
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 500 additions and 36 deletions

View File

@ -23,7 +23,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"knative.dev/client/pkg/kn/config" "knative.dev/client/pkg/kn/config"
"knative.dev/client/pkg/kn/plugin" pluginpkg "knative.dev/client/pkg/kn/plugin"
"knative.dev/client/pkg/kn/root" "knative.dev/client/pkg/kn/root"
) )
@ -60,7 +60,7 @@ func run(args []string) error {
return err return err
} }
pluginManager := plugin.NewManager(config.GlobalConfig.PluginsDir(), config.GlobalConfig.LookupPluginsInPath()) pluginManager := pluginpkg.NewManager(config.GlobalConfig.PluginsDir(), config.GlobalConfig.LookupPluginsInPath())
// Create kn root command and all sub-commands // Create kn root command and all sub-commands
rootCmd, err := root.NewRootCommand(pluginManager.HelpTemplateFuncs()) rootCmd, err := root.NewRootCommand(pluginManager.HelpTemplateFuncs())
@ -84,13 +84,37 @@ func run(args []string) error {
return err return err
} }
// FT: Context Sharing
var ctxManager *pluginpkg.ContextDataManager
if config.GlobalConfig.ContextSharing() {
ctxManager, err = pluginpkg.NewContextManager(pluginManager)
if err != nil {
return err
}
defer func(ctxManager *pluginpkg.ContextDataManager) {
if err := ctxManager.WriteCache(); err != nil {
println("error during write")
}
}(ctxManager)
}
if plugin != nil { if plugin != nil {
// Validate & Execute plugin // Validate & Execute plugin
err = validatePlugin(rootCmd, plugin) err = validatePlugin(rootCmd, plugin)
if err != nil { if err != nil {
return err return err
} }
if config.GlobalConfig.ContextSharing() {
if pwm, ok := plugin.(pluginpkg.PluginWithManifest); ok {
data, _ := ctxManager.FetchContextData()
err := pwm.ExecuteWithContext(data, argsWithoutCommands(args, plugin.CommandParts()))
if err != nil {
return &runError{err: err}
}
}
return nil
}
err := plugin.Execute(argsWithoutCommands(args, plugin.CommandParts())) err := plugin.Execute(argsWithoutCommands(args, plugin.CommandParts()))
if err != nil { if err != nil {
return &runError{err: err} return &runError{err: err}
@ -140,7 +164,7 @@ func filterHelpOptions(args []string) []string {
} }
// Check if the plugin collides with any command specified in the root command // Check if the plugin collides with any command specified in the root command
func validatePlugin(root *cobra.Command, plugin plugin.Plugin) error { func validatePlugin(root *cobra.Command, plugin pluginpkg.Plugin) error {
// Check if a given plugin can be identified as a command // Check if a given plugin can be identified as a command
cmd, args, err := root.Find(plugin.CommandParts()) cmd, args, err := root.Find(plugin.CommandParts())

View File

@ -21,7 +21,7 @@ import (
"strings" "strings"
"testing" "testing"
"knative.dev/client/pkg/kn/plugin" pluginpkg "knative.dev/client/pkg/kn/plugin"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
@ -361,7 +361,7 @@ func TestRun(t *testing.T) {
args []string args []string
expectedOut []string expectedOut []string
expectedErrOut []string expectedErrOut []string
plugin plugin.Plugin plugin pluginpkg.Plugin
}{ }{
{ {
[]string{"kn", "version"}, []string{"kn", "version"},
@ -425,8 +425,8 @@ func TestRun(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
os.Args = tc.args os.Args = tc.args
if tc.plugin != nil { if tc.plugin != nil {
plugin.InternalPlugins = plugin.PluginList{} pluginpkg.InternalPlugins = pluginpkg.PluginList{}
plugin.InternalPlugins = append(plugin.InternalPlugins, tc.plugin) pluginpkg.InternalPlugins = append(pluginpkg.InternalPlugins, tc.plugin)
} }
capture := test.CaptureOutput(t) capture := test.CaptureOutput(t)
err := run(tc.args[1:]) err := run(tc.args[1:])

View File

@ -28,6 +28,7 @@ import (
"knative.dev/serving/pkg/apis/serving" "knative.dev/serving/pkg/apis/serving"
"knative.dev/client/pkg/kn/commands/revision" "knative.dev/client/pkg/kn/commands/revision"
"knative.dev/client/pkg/kn/plugin"
"knative.dev/client/pkg/printers" "knative.dev/client/pkg/printers"
clientservingv1 "knative.dev/client/pkg/serving/v1" clientservingv1 "knative.dev/client/pkg/serving/v1"
@ -99,10 +100,21 @@ func NewServiceDescribeCommand(p *commands.KnParams) *cobra.Command {
Example: describe_example, Example: describe_example,
ValidArgsFunction: commands.ResourceNameCompletionFunc(p), ValidArgsFunction: commands.ResourceNameCompletionFunc(p),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
var serviceName string
if len(args) != 1 { if len(args) != 1 {
if plugin.CtxManager != nil {
data, err := plugin.CtxManager.FetchContextData()
if err != nil {
return err
}
serviceName = data["service"]
}
if serviceName == "" {
return errors.New("'service describe' requires the service name given as single argument") return errors.New("'service describe' requires the service name given as single argument")
} }
serviceName := args[0] } else {
serviceName = args[0]
}
namespace, err := p.GetNamespace(cmd) namespace, err := p.GetNamespace(cmd)
if err != nil { if err != nil {
@ -182,7 +194,7 @@ func describe(w io.Writer, service *servingv1.Service, revisions []*revisionDesc
return nil return nil
} }
// Write out main service information. Use colors for major items. // WriteCache out main service information. Use colors for major items.
func writeService(dw printers.PrefixWriter, service *servingv1.Service) { func writeService(dw printers.PrefixWriter, service *servingv1.Service) {
commands.WriteMetadata(dw, &service.ObjectMeta, printDetails) commands.WriteMetadata(dw, &service.ObjectMeta, printDetails)
dw.WriteAttribute("URL", extractURL(service)) dw.WriteAttribute("URL", extractURL(service))
@ -200,7 +212,7 @@ func writeService(dw printers.PrefixWriter, service *servingv1.Service) {
} }
} }
// Write out revisions associated with this service. By default only active // WriteCache out revisions associated with this service. By default only active
// target revisions are printed, but with --verbose also inactive revisions // target revisions are printed, but with --verbose also inactive revisions
// created by this services are shown // created by this services are shown
func writeRevisions(dw printers.PrefixWriter, revisions []*revisionDesc, printDetails bool) { func writeRevisions(dw printers.PrefixWriter, revisions []*revisionDesc, printDetails bool) {

View File

@ -55,6 +55,7 @@ const configContentDefaults = `# Taken from https://github.com/knative/client/bl
// config contains the variables for the Kn config // config contains the variables for the Kn config
type config struct { type config struct {
// configFile is the config file location // configFile is the config file location
configFile string configFile string
@ -65,6 +66,10 @@ type config struct {
channelTypeMappings []ChannelTypeMapping channelTypeMappings []ChannelTypeMapping
} }
func (c *config) ContextSharing() bool {
return viper.GetBool(keyFeaturesContextSharing)
}
// ConfigFile returns the config file which is either the default XDG conform // ConfigFile returns the config file which is either the default XDG conform
// config file location or the one set with --config // config file location or the one set with --config
func (c *config) ConfigFile() string { func (c *config) ConfigFile() string {

View File

@ -117,7 +117,7 @@ func setupConfig(t *testing.T, configContent string) (string, func()) {
// Save old args // Save old args
backupArgs := os.Args backupArgs := os.Args
// Write out a temporary configContent file // WriteCache out a temporary configContent file
var cfgFile string var cfgFile string
if configContent != "" { if configContent != "" {
cfgFile = filepath.Join(tmpDir, "config.yaml") cfgFile = filepath.Join(tmpDir, "config.yaml")

View File

@ -18,6 +18,7 @@ package config
// Set an instance of this for config.GlobalConfig to mock // Set an instance of this for config.GlobalConfig to mock
// your own configuration setup // your own configuration setup
type TestConfig struct { type TestConfig struct {
TestContextSharing bool
TestPluginsDir string TestPluginsDir string
TestConfigFile string TestConfigFile string
TestLookupPluginsInPath bool TestLookupPluginsInPath bool
@ -28,6 +29,7 @@ type TestConfig struct {
// Ensure that TestConfig implements the configuration interface // Ensure that TestConfig implements the configuration interface
var _ Config = &TestConfig{} var _ Config = &TestConfig{}
func (t TestConfig) ContextSharing() bool { return t.TestContextSharing }
func (t TestConfig) PluginsDir() string { return t.TestPluginsDir } func (t TestConfig) PluginsDir() string { return t.TestPluginsDir }
func (t TestConfig) ConfigFile() string { return t.TestConfigFile } func (t TestConfig) ConfigFile() string { return t.TestConfigFile }
func (t TestConfig) LookupPluginsInPath() bool { return t.TestLookupPluginsInPath } func (t TestConfig) LookupPluginsInPath() bool { return t.TestLookupPluginsInPath }

View File

@ -23,6 +23,7 @@ import (
// Test to keep code coverage quality gate happy. // Test to keep code coverage quality gate happy.
func TestTestConfig(t *testing.T) { func TestTestConfig(t *testing.T) {
cfg := TestConfig{ cfg := TestConfig{
TestContextSharing: false,
TestPluginsDir: "pluginsDir", TestPluginsDir: "pluginsDir",
TestConfigFile: "configFile", TestConfigFile: "configFile",
TestLookupPluginsInPath: true, TestLookupPluginsInPath: true,
@ -30,6 +31,7 @@ func TestTestConfig(t *testing.T) {
TestChannelTypeMappings: nil, TestChannelTypeMappings: nil,
} }
assert.Equal(t, cfg.ContextSharing(), false)
assert.Equal(t, cfg.PluginsDir(), "pluginsDir") assert.Equal(t, cfg.PluginsDir(), "pluginsDir")
assert.Equal(t, cfg.ConfigFile(), "configFile") assert.Equal(t, cfg.ConfigFile(), "configFile")
assert.Assert(t, cfg.LookupPluginsInPath()) assert.Assert(t, cfg.LookupPluginsInPath())

View File

@ -19,6 +19,9 @@ package config
type Config interface { type Config interface {
// ContextSharing represents feature flag enabling context sharing
ContextSharing() bool
// ConfigFile returns the location of the configuration file // ConfigFile returns the location of the configuration file
ConfigFile() string ConfigFile() string
@ -70,6 +73,7 @@ type ChannelTypeMapping struct {
// config Keys for looking up in viper // config Keys for looking up in viper
const ( const (
keyFeaturesContextSharing = "features.context-sharing"
keyPluginsDirectory = "plugins.directory" keyPluginsDirectory = "plugins.directory"
keySinkMappings = "eventing.sink-mappings" keySinkMappings = "eventing.sink-mappings"
keyChannelTypeMappings = "eventing.channel-type-mappings" keyChannelTypeMappings = "eventing.channel-type-mappings"

View File

@ -0,0 +1,232 @@
// Copyright © 2023 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"bytes"
"encoding/json"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
"knative.dev/client/pkg/kn/config"
)
//--TYPES--
// Manifest represents plugin metadata
type Manifest struct {
// Path to external plugin binary. Always empty for inlined plugins.
Path string `json:"path,omitempty"`
// Plugin declares its own manifest to be included in Context Sharing feature
HasManifest bool `json:"hasManifest"`
// ProducesContextDataKeys is a list of keys for the ContextData that
// a plugin can produce. Nil or an empty list declares that this
// plugin is not ContextDataProducer
ProducesContextDataKeys []string `json:"producesKeys,omitempty"`
// ConsumesContextDataKeys is a list of keys from a ContextData that a
// plugin is interested in to consume. Nil or an empty list declares
// that this plugin is not a ContextDataConsumer
ConsumesContextDataKeys []string `json:"consumesKeys,omitempty"`
}
// PluginWithManifest represents extended plugin support for Manifest and Context Sharing feature
type PluginWithManifest interface {
// Plugin original interface wrapper
Plugin
// GetManifest
GetManifest() *Manifest
// GetContextData
GetContextData() map[string]string
// ExecuteWithContext
ExecuteWithContext(ctx map[string]string, args []string) error
}
//--TYPES--
var CtxManager *ContextDataManager
type ContextDataManager struct {
//ContextData map[string]ContextData `json:"-"`
PluginManager *Manager `json:"-"`
Producers map[string][]string `json:"producers"`
Consumers map[string][]string `json:"consumers"`
Manifests map[string]Manifest `json:"manifests"`
}
func NewContextManager(pluginManager *Manager) (*ContextDataManager, error) {
if CtxManager == nil {
CtxManager = &ContextDataManager{
PluginManager: pluginManager,
Producers: map[string][]string{},
Consumers: map[string][]string{},
Manifests: map[string]Manifest{},
}
}
return CtxManager, nil
}
// GetConsumesKeys returns array of keys consumed by plugin
func (c *ContextDataManager) GetConsumesKeys(pluginName string) []string {
return c.Manifests[pluginName].ConsumesContextDataKeys
}
// GetProducesKeys returns array of keys produced by plugin
func (c *ContextDataManager) GetProducesKeys(pluginName string) []string {
return c.Manifests[pluginName].ProducesContextDataKeys
}
func (c *ContextDataManager) FetchContextData() (map[string]string, error) {
// Load cached data first
if err := c.loadCache(); err != nil {
return nil, err
}
// Fetch manifests
if err := c.FetchManifests(); err != nil {
return nil, err
}
// Get context data, limited to func only
for _, p := range c.PluginManager.GetInternalPlugins() {
if p.Name() == "kn func" {
if pwm, ok := p.(PluginWithManifest); ok {
return pwm.GetContextData(), nil
}
}
}
return map[string]string{}, nil
}
// FetchManifests it tries to retrieve manifest from both inlined and external plugins
func (c *ContextDataManager) FetchManifests() error {
for _, plugin := range c.PluginManager.GetInternalPlugins() {
manifest := &Manifest{}
// For the integrity build the same name format as external plugins
pluginName := "kn-" + strings.Join(plugin.CommandParts(), "-")
if pwm, ok := plugin.(PluginWithManifest); ok {
manifest = pwm.GetManifest()
}
// Add manifest to map
c.Manifests[pluginName] = *manifest
if manifest.HasManifest {
c.populateDataKeys(manifest, pluginName)
}
}
plugins, err := c.PluginManager.ListPlugins()
if err != nil {
return err
}
for _, plugin := range plugins {
// Add new plugins only
if _, exists := c.Manifests[plugin.Name()]; !exists {
if plugin.Path() != "" {
manifest := &Manifest{
Path: plugin.Path(),
}
// Fetch from external plugin
if m := fetchExternalManifest(plugin); m != nil {
manifest = m
}
// Add manifest to map
c.Manifests[plugin.Name()] = *manifest
if manifest.HasManifest {
c.populateDataKeys(manifest, plugin.Name())
}
}
}
}
return nil
}
func (c *ContextDataManager) populateDataKeys(manifest *Manifest, pluginName string) {
// Build producers mapping
for _, key := range manifest.ProducesContextDataKeys {
c.Producers[key] = append(c.Producers[key], pluginName)
}
// Build consumers mapping
for _, key := range manifest.ConsumesContextDataKeys {
c.Consumers[key] = append(c.Consumers[key], pluginName)
}
}
// TODO: We should cautiously execute external binaries
// fetchExternalManifest returns Manifest from external plugin by exec `$plugin manifest get`
func fetchExternalManifest(p Plugin) *Manifest {
cmd := exec.Command(p.Path(), "manifest") //nolint:gosec
stdOut := new(bytes.Buffer)
cmd.Stdout = stdOut
manifest := &Manifest{
Path: p.Path(),
}
if err := cmd.Run(); err != nil {
return nil
}
d := json.NewDecoder(stdOut)
if err := d.Decode(manifest); err != nil {
return nil
}
manifest.HasManifest = true
return manifest
}
func (c *ContextDataManager) loadCache() error {
cacheFile := filepath.Join(filepath.Dir(config.GlobalConfig.ConfigFile()), "context.json")
if _, err := os.Stat(cacheFile); err != nil {
if os.IsNotExist(err) {
return nil // No cache file yet
} else {
return err
}
}
file, err := os.Open(cacheFile)
if err != nil {
return err
}
decoder := json.NewDecoder(file)
ctxManager := &ContextDataManager{}
if err := decoder.Decode(ctxManager); err != nil {
return err
}
c.Manifests = ctxManager.Manifests
c.Producers = ctxManager.Producers
c.Consumers = ctxManager.Consumers
return nil
}
// WriteCache store data back to cache file
func (c *ContextDataManager) WriteCache() error {
out := new(bytes.Buffer)
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
if err := enc.Encode(c); err != nil {
return nil
}
return os.WriteFile(filepath.Join(filepath.Dir(config.GlobalConfig.ConfigFile()), "context.json"), out.Bytes(), fs.FileMode(0664))
}

View File

@ -0,0 +1,184 @@
// Copyright © 2023 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !windows
package plugin
import (
"os"
"path/filepath"
"strings"
"testing"
"gotest.tools/v3/assert"
)
type testPluginWithManifest struct {
testPlugin
manifest *Manifest
contextData map[string]string
}
func (t testPluginWithManifest) GetManifest() *Manifest { return t.manifest }
func (t testPluginWithManifest) GetContextData() map[string]string { return t.contextData }
func (t testPluginWithManifest) ExecuteWithContext(ctx map[string]string, args []string) error {
return nil
}
// Verify both interfaces are implemented
var _ Plugin = testPluginWithManifest{}
var _ PluginWithManifest = testPluginWithManifest{}
func TestFetchManifest(t *testing.T) {
testCases := []struct {
name string
cmdPart []string
hasManifest bool
expectedManifest *Manifest
}{
{
name: "Inlined with manifest",
cmdPart: []string{"cmd"},
hasManifest: true,
expectedManifest: &Manifest{
Path: "",
HasManifest: true,
ProducesContextDataKeys: []string{"service"},
ConsumesContextDataKeys: []string{"service"},
},
},
{
name: "Inlined no manifest",
cmdPart: []string{"no", "manifest"},
hasManifest: false,
expectedManifest: &Manifest{
HasManifest: false,
},
},
{
name: "No plugins",
cmdPart: []string{},
hasManifest: false,
expectedManifest: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c := setup(t)
t.Cleanup(func() {
cleanup(t, c)
InternalPlugins = PluginList{}
CtxManager = nil
})
if len(tc.cmdPart) == 0 {
ctxManager, err := NewContextManager(c.pluginManager)
assert.NilError(t, err)
err = ctxManager.FetchManifests()
assert.NilError(t, err)
assert.Assert(t, len(ctxManager.Manifests) == 0)
return
}
if tc.hasManifest {
prepareInternalPlugins(testPluginWithManifest{
testPlugin: testPlugin{parts: tc.cmdPart},
manifest: tc.expectedManifest,
contextData: map[string]string{},
})
} else {
prepareInternalPlugins(testPlugin{parts: tc.cmdPart})
}
ctxManager, err := NewContextManager(c.pluginManager)
assert.NilError(t, err)
err = ctxManager.FetchManifests()
assert.NilError(t, err)
assert.Assert(t, len(ctxManager.Manifests) == 1)
expectedKey := "kn-" + strings.Join(tc.cmdPart, "-")
assert.DeepEqual(t, ctxManager.Manifests[expectedKey], *tc.expectedManifest)
})
}
}
func TestContextFetchExternalManifests(t *testing.T) {
testCases := []struct {
name string
testScript string
expectedManifest *Manifest
}{
{
name: "manifest",
testScript: `#!/bin/bash
echo '{"hasManifest":true,"consumesKeys":["service"]}'\n`,
expectedManifest: &Manifest{
HasManifest: true,
ConsumesContextDataKeys: []string{"service"},
},
},
{
name: "badjson",
testScript: `#!/bin/bash
echo '{hasManifest:true,"consumesKeys":["service"]}'\n`,
expectedManifest: nil,
},
{
name: "badscript",
testScript: `#!/bin/bash
exit 1\n`,
expectedManifest: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ct := setup(t)
defer cleanup(t, ct)
fullPath := createTestPluginInDirectoryFromScript(t, "kn-"+tc.name, ct.pluginsDir, tc.testScript)
// Set expected path
if tc.expectedManifest != nil {
tc.expectedManifest.Path = fullPath
}
testPlugin, err := ct.pluginManager.FindPlugin([]string{tc.name})
assert.NilError(t, err)
assert.Assert(t, testPlugin != nil)
actual := fetchExternalManifest(testPlugin)
assert.DeepEqual(t, actual, tc.expectedManifest)
})
}
}
// CreateTestPluginInPath with name, path, script, and fileMode and return the tmp random path
func createTestPluginInDirectoryFromScript(t *testing.T, name string, dir string, script string) string {
fullPath := filepath.Join(dir, name)
err := os.WriteFile(fullPath, []byte(script), 0777)
assert.NilError(t, err)
// Some extra files to feed the tests
err = os.WriteFile(filepath.Join(dir, "non-plugin-prefix-"+name), []byte{}, 0555)
assert.NilError(t, err)
_, err = os.CreateTemp(dir, "bogus-dir")
assert.NilError(t, err)
return fullPath
}

View File

@ -0,0 +1,15 @@
// Copyright © 2023 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin

View File

@ -55,26 +55,6 @@ type Plugin interface {
Path() string Path() string
} }
type ContextData map[string]string
type PluginManifest struct {
// ProducesContextDataKeys is a list of keys for the ContextData that
// a plugin can produce. Nil or an empty list declares that this
// plugin is not ContextDataProducer
ProducesContextDataKeys []string
// ConsumesContextDataKeys is a list of keys from a ContextData that a
// plugin is interested in to consume. Nil or an empty list declares
// that this plugin is not a ContextDataConsumer
ConsumesContextDataKeys []string
}
type ContextDataConsumer interface {
// ExecuteWithContextData executes the plugin with the given args much like
// Execute() but with an additional argument that holds the ContextData
ExecuteWithContextData(args []string, data ContextData) error
}
type Manager struct { type Manager struct {
// Dedicated plugin directory as configured // Dedicated plugin directory as configured
pluginsDir string pluginsDir string
@ -125,6 +105,10 @@ func (manager *Manager) AppendPlugin(plugin Plugin) {
InternalPlugins = append(InternalPlugins, plugin) InternalPlugins = append(InternalPlugins, plugin)
} }
func (manager *Manager) GetInternalPlugins() PluginList {
return InternalPlugins
}
// FindPlugin checks if a plugin for the given parts exist and return it. // FindPlugin checks if a plugin for the given parts exist and return it.
// The args given must not contain any options and contain only // The args given must not contain any options and contain only
// the commands (like in [ "source", "github" ] for a plugin called 'kn-source-github' // the commands (like in [ "source", "github" ] for a plugin called 'kn-source-github'