Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to list plugins #2319

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`);
console.log(stdout.toString().trim());
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)
knrt10 marked this conversation as resolved.
Show resolved Hide resolved
}

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"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yolossn This is not really a config option. It's rather a subcommand.
So I wonder if we shouldn't include it in the koanf config. Do you know if there's an option to modify that in koanf, or should we use the flag parsing in a different way?

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
49 changes: 46 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,39 @@ 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)
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
Loading