Skip to content

Commit

Permalink
Add option to list plugins
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
skoeva committed Oct 2, 2024
1 parent 488fdf4 commit 7732503
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 3 deletions.
18 changes: 18 additions & 0 deletions app/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions backend/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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()

Expand Down
2 changes: 2 additions & 0 deletions backend/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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")
Expand Down
50 changes: 47 additions & 3 deletions backend/pkg/plugins/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,30 @@ 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 != "" {
var err error

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
}
Expand All @@ -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)
Expand Down
84 changes: 84 additions & 0 deletions backend/pkg/plugins/plugins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package plugins_test

import (
"context"
"io"
"net/http/httptest"
"os"
"path"
Expand Down Expand Up @@ -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/")
Expand Down

0 comments on commit 7732503

Please sign in to comment.