From 0834cd7dccd8522ba9cb54899b089bbb3ef88d86 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Mon, 9 Sep 2024 17:42:17 -0400 Subject: [PATCH] Add option to list plugins This change creates a CLI option in the backend + desktop to list all plugins, with a distinction between static/shipped plugins and user-added ones. Fixes: #2161 Signed-off-by: Evangelos Skopelitis --- app/electron/main.ts | 35 +++++++++++++++++++++ backend/cmd/server.go | 10 ++++++ backend/pkg/config/config.go | 2 ++ backend/pkg/plugins/plugins.go | 47 +++++++++++++++++++++++++++-- backend/pkg/plugins/plugins_test.go | 16 ++++++++++ 5 files changed, 107 insertions(+), 3 deletions(-) diff --git a/app/electron/main.ts b/app/electron/main.ts index bab5cd2024..56a892b3ba 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -69,6 +69,10 @@ const args = yargs(hideBin(process.argv)) describe: 'Disable use of GPU. For people who may have buggy graphics drivers', type: 'boolean', }, + 'list-plugins': { + describe: 'List all static and user-added plugins.', + type: 'boolean', + }, }) .positional('kubeconfig', { describe: @@ -78,6 +82,37 @@ const args = yargs(hideBin(process.argv)) .help() .parseSync(); +const listPlugins = args['list-plugins'] === true; + +// If the user wants to list plugins, we don't need to start the app. +if (listPlugins) { + const staticDir = path.join(process.resourcesPath, '.plugins'); + const pluginsDir = path.join(app.getPath('userData'), 'plugins'); + + const staticPlugins = fs.existsSync(staticDir) ? fs.readdirSync(staticDir) : []; + const userPlugins = fs.existsSync(pluginsDir) ? fs.readdirSync(pluginsDir) : []; + + if (staticPlugins.length > 0) { + console.log(` Static Plugins (${staticDir}):`); + staticPlugins.forEach(plugin => { + console.log(` - ${plugin}`); + }); + } else { + console.log(` No static plugins found.`); + } + + if (userPlugins.length > 0) { + console.log(` User-added Plugins (${pluginsDir}):`); + userPlugins.forEach(plugin => { + console.log(` - ${plugin}`); + }); + } else { + console.log(` No user-added plugins found.`); + } + + process.exit(0); +} + const isHeadlessMode = args.headless === true; let disableGPU = args['disable-gpu'] === true; const defaultPort = 4466; diff --git a/backend/cmd/server.go b/backend/cmd/server.go index 2959990c40..cce1cf5a44 100644 --- a/backend/cmd/server.go +++ b/backend/cmd/server.go @@ -8,6 +8,7 @@ import ( "github.com/headlamp-k8s/headlamp/backend/pkg/config" "github.com/headlamp-k8s/headlamp/backend/pkg/kubeconfig" "github.com/headlamp-k8s/headlamp/backend/pkg/logger" + "github.com/headlamp-k8s/headlamp/backend/pkg/plugins" ) func main() { @@ -17,6 +18,15 @@ func main() { os.Exit(1) } + if conf.ListPlugins { + if err := plugins.ListPlugins(conf.StaticDir, conf.PluginsDir); err != nil { + logger.Log(logger.LevelError, nil, err, "listing plugins") + os.Exit(1) + } + + os.Exit(0) + } + cache := cache.New[interface{}]() kubeConfigStore := kubeconfig.NewContextStore() diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index bb110b897e..bebd87f423 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -25,6 +25,7 @@ type Config struct { InsecureSsl bool `koanf:"insecure-ssl"` EnableHelm bool `koanf:"enable-helm"` EnableDynamicClusters bool `koanf:"enable-dynamic-clusters"` + ListPlugins bool `koanf:"list-plugins"` Port uint `koanf:"port"` KubeConfigPath string `koanf:"kubeconfig"` StaticDir string `koanf:"html-static-dir"` @@ -153,6 +154,7 @@ func flagset() *flag.FlagSet { f.Bool("dev", false, "Allow connections from other origins") f.Bool("insecure-ssl", false, "Accept/Ignore all server SSL certificates") f.Bool("enable-dynamic-clusters", false, "Enable dynamic clusters, which stores stateless clusters in the frontend.") + f.Bool("list-plugins", false, "List all static and user-added plugins") f.String("kubeconfig", "", "Absolute path to the kubeconfig file") f.String("html-static-dir", "", "Static HTML directory to serve") diff --git a/backend/pkg/plugins/plugins.go b/backend/pkg/plugins/plugins.go index 431052af21..f3954f7c7b 100644 --- a/backend/pkg/plugins/plugins.go +++ b/backend/pkg/plugins/plugins.go @@ -77,8 +77,8 @@ func periodicallyWatchSubfolders(watcher *fsnotify.Watcher, path string, interva } } -// GeneratePluginPaths takes the staticPluginDir and pluginDir and returns a list of plugin paths. -func GeneratePluginPaths(staticPluginDir string, pluginDir string) ([]string, error) { +// generateSeparatePluginPaths takes the staticPluginDir and pluginDir and returns separate lists of plugin paths. +func generateSeparatePluginPaths(staticPluginDir, pluginDir string) ([]string, []string, error) { var pluginListURLStatic []string if staticPluginDir != "" { @@ -86,11 +86,21 @@ func GeneratePluginPaths(staticPluginDir string, pluginDir string) ([]string, er pluginListURLStatic, err = pluginBasePathListForDir(staticPluginDir, "static-plugins") if err != nil { - return nil, err + return nil, nil, err } } pluginListURL, err := pluginBasePathListForDir(pluginDir, "plugins") + if err != nil { + return nil, nil, err + } + + return pluginListURLStatic, pluginListURL, nil +} + +// GeneratePluginPaths generates a concatenated list of plugin paths from the staticPluginDir and pluginDir. +func GeneratePluginPaths(staticPluginDir, pluginDir string) ([]string, error) { + pluginListURLStatic, pluginListURL, err := generateSeparatePluginPaths(staticPluginDir, pluginDir) if err != nil { return nil, err } @@ -103,6 +113,37 @@ func GeneratePluginPaths(staticPluginDir string, pluginDir string) ([]string, er return pluginListURL, nil } +var GenerateSeparatePluginPathsFunc = generateSeparatePluginPaths + +// ListPlugins lists the plugins in the static and user-added plugin directories. +func ListPlugins(staticPluginDir, pluginDir string) error { + staticPlugins, userPlugins, err := GenerateSeparatePluginPathsFunc(staticPluginDir, pluginDir) + if err != nil { + logger.Log(logger.LevelError, nil, err, "listing plugins") + return fmt.Errorf("listing plugins: %w", err) + } + + if len(staticPlugins) > 0 { + fmt.Printf(" Static Plugins (%s):\n", staticPluginDir) + for _, plugin := range staticPlugins { + fmt.Println(" -", plugin) + } + } else { + fmt.Printf(" No static plugins found.\n") + } + + if len(userPlugins) > 0 { + fmt.Printf(" User-added Plugins (%s):\n", pluginDir) + for _, plugin := range userPlugins { + fmt.Println(" -", plugin) + } + } else { + fmt.Printf(" No user-added plugins found.\n") + } + + return nil +} + // pluginBasePathListForDir returns a list of valid plugin paths for the given directory. func pluginBasePathListForDir(pluginDir string, baseURL string) ([]string, error) { files, err := os.ReadDir(pluginDir) diff --git a/backend/pkg/plugins/plugins_test.go b/backend/pkg/plugins/plugins_test.go index e01aca81ee..d8c2016ad3 100644 --- a/backend/pkg/plugins/plugins_test.go +++ b/backend/pkg/plugins/plugins_test.go @@ -178,6 +178,22 @@ func TestGeneratePluginPaths(t *testing.T) { //nolint:funlen require.NoError(t, err) } +func TestListPlugins(t *testing.T) { + originalFunc := plugins.GenerateSeparatePluginPathsFunc + defer func() { plugins.GenerateSeparatePluginPathsFunc = originalFunc }() + + // mock the GenerateSeparatePluginPaths function to create lists of plugins + plugins.GenerateSeparatePluginPathsFunc = func(staticPluginDir string, pluginDir string) ([]string, []string, error) { + staticPlugins := []string{"static-plugin-1", "static-plugin-2"} + userPlugins := []string{"user-plugin-1", "user-plugin-2"} + + return staticPlugins, userPlugins, nil + } + + err := plugins.ListPlugins("", "") + require.NoError(t, err) +} + func TestHandlePluginEvents(t *testing.T) { //nolint:funlen // Create a temporary directory if it doesn't exist _, err := os.Stat("/tmp/")