diff --git a/app/electron/main.ts b/app/electron/main.ts index bab5cd20243..a6b08d46ebf 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,20 @@ const args = yargs(hideBin(process.argv)) .help() .parseSync(); +const listPlugins = args['list-plugins'] === true; + +if (listPlugins) { + try { + const backendPath = path.join(process.resourcesPath, 'headlamp-server'); + const stdout = execSync(`${backendPath} --list-plugins`); + process.stdout.write(stdout); + process.exit(0); + } catch (error) { + console.error(`Error listing plugins: ${error}`); + process.exit(1); + } +} + 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 2959990c40a..cce1cf5a440 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 bb110b897e9..bebd87f4231 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 431052af218..c32e925d2e2 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,40 @@ 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.Println("No static plugins found.") + } + + if len(userPlugins) > 0 { + fmt.Printf("\nUser-added Plugins (%s):\n", pluginDir) + + for _, plugin := range userPlugins { + plugin = strings.TrimPrefix(plugin, "plugins/") + fmt.Println(" -", plugin) + } + } else { + fmt.Printf("No user-added plugins found.") + } + + 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 e01aca81eee..cc53f90d776 100644 --- a/backend/pkg/plugins/plugins_test.go +++ b/backend/pkg/plugins/plugins_test.go @@ -2,6 +2,7 @@ package plugins_test import ( "context" + "io" "net/http/httptest" "os" "path" @@ -178,6 +179,89 @@ func TestGeneratePluginPaths(t *testing.T) { //nolint:funlen require.NoError(t, err) } +func captureOutput(f func()) (string, error) { + r, w, err := os.Pipe() + if err != nil { + return "", err + } + + originalStdout := os.Stdout + os.Stdout = w + + f() + + err = w.Close() + if err != nil { + return "", err + } + + os.Stdout = originalStdout + + outputBytes, err := io.ReadAll(r) + if err != nil { + return "", err + } + + return string(outputBytes), nil +} + +func TestListPlugins(t *testing.T) { + // Create a temporary directory if it doesn't exist + _, err := os.Stat("/tmp/") + if os.IsNotExist(err) { + err = os.Mkdir("/tmp/", 0o755) + require.NoError(t, err) + } + + // create a static plugin directory in /tmp + staticPluginDir := path.Join("/tmp", uuid.NewString()) + err = os.Mkdir(staticPluginDir, 0o755) + require.NoError(t, err) + + staticPlugin1Dir := path.Join(staticPluginDir, "static-plugin-1") + err = os.Mkdir(staticPlugin1Dir, 0o755) + require.NoError(t, err) + + // Create main.js and package.json for static plugin + staticMainJsPath := path.Join(staticPlugin1Dir, "main.js") + _, err = os.Create(staticMainJsPath) + require.NoError(t, err) + + staticPackageJSONPath := path.Join(staticPlugin1Dir, "package.json") + _, err = os.Create(staticPackageJSONPath) + require.NoError(t, err) + + // create a user plugin directory in /tmp + pluginDir := path.Join("/tmp", uuid.NewString()) + err = os.Mkdir(pluginDir, 0o755) + require.NoError(t, err) + + plugin1Dir := path.Join(pluginDir, "user-plugin-1") + err = os.Mkdir(plugin1Dir, 0o755) + require.NoError(t, err) + + // Create main.js and package.json for user plugin + userMainJsPath := path.Join(plugin1Dir, "main.js") + _, err = os.Create(userMainJsPath) + require.NoError(t, err) + + packageJSONPath := path.Join(plugin1Dir, "package.json") + _, err = os.Create(packageJSONPath) + require.NoError(t, err) + + // Capture the output of the ListPlugins function + output, err := captureOutput(func() { + err := plugins.ListPlugins(staticPluginDir, pluginDir) + require.NoError(t, err) + }) + require.NoError(t, err) + + require.Contains(t, output, "Static Plugins") + require.Contains(t, output, "static-plugin-1") + require.Contains(t, output, "User-added Plugins") + require.Contains(t, output, "user-plugin-1") +} + func TestHandlePluginEvents(t *testing.T) { //nolint:funlen // Create a temporary directory if it doesn't exist _, err := os.Stat("/tmp/")