mirror of https://github.com/knative/client.git
Context Sharing POC (#1855)
* WIP: Context Sharing POC * More lazy init * Fix imports
This commit is contained in:
parent
c318a2224c
commit
b658574638
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:])
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
return errors.New("'service describe' requires the service name given as single argument")
|
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")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
serviceName = args[0]
|
||||||
}
|
}
|
||||||
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) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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,9 +73,10 @@ type ChannelTypeMapping struct {
|
||||||
|
|
||||||
// config Keys for looking up in viper
|
// config Keys for looking up in viper
|
||||||
const (
|
const (
|
||||||
keyPluginsDirectory = "plugins.directory"
|
keyFeaturesContextSharing = "features.context-sharing"
|
||||||
keySinkMappings = "eventing.sink-mappings"
|
keyPluginsDirectory = "plugins.directory"
|
||||||
keyChannelTypeMappings = "eventing.channel-type-mappings"
|
keySinkMappings = "eventing.sink-mappings"
|
||||||
|
keyChannelTypeMappings = "eventing.channel-type-mappings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// legacy config keys, deprecated
|
// legacy config keys, deprecated
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue