Skip to content

Commit

Permalink
Add option to disable plugins
Browse files Browse the repository at this point in the history
This change creates a CLI option in the backend + desktop to disable plugins.

Fixes: #2162

Signed-off-by: Evangelos Skopelitis <[email protected]>
  • Loading branch information
skoeva committed Sep 18, 2024
1 parent 7f3e899 commit 845968d
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 27 deletions.
10 changes: 10 additions & 0 deletions app/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ const args = yargs(hideBin(process.argv))
describe: 'Disable use of GPU. For people who may have buggy graphics drivers',
type: 'boolean',
},
'disable-plugins': {
describe: 'Disable specific plugins or all plugins if no argument is provided',
type: 'string',
coerce: arg => {
if (typeof arg === 'undefined') {
return 'all';
}
return arg.split(',');
},
},
})
.positional('kubeconfig', {
describe:
Expand Down
35 changes: 33 additions & 2 deletions app/electron/plugin-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export class PluginManager {
* Installs a plugin from the specified URL.
* @param {string} URL - The URL of the plugin to install.
* @param {string} [destinationFolder=defaultPluginsDir()] - The folder where the plugin will be installed.
* @param {string[]} [disabledPlugins = []] - An array of disabled plugins.
* @param {string} [headlampVersion=""] - The version of Headlamp for compatibility checking.
* @param {function} [progressCallback=null] - Optional callback for progress updates.
* @param {AbortSignal} [signal=null] - Optional AbortSignal for cancellation.
Expand All @@ -81,6 +82,7 @@ export class PluginManager {
static async install(
URL,
destinationFolder = defaultPluginsDir(),
disabledPlugins: string[] = [],
headlampVersion = '',
progressCallback: null | ProgressCallback = null,
signal: AbortSignal | null = null
Expand All @@ -93,6 +95,10 @@ export class PluginManager {
signal
);

if (disabledPlugins.includes(name)) {
throw new Error(`Plugin ${name} is disabled`);
}

// sleep(2000); // comment out for testing

// create the destination folder if it doesn't exist
Expand All @@ -119,6 +125,7 @@ export class PluginManager {
* Updates an installed plugin to the latest version.
* @param {string} pluginName - The name of the plugin to update.
* @param {string} [destinationFolder=defaultPluginsDir()] - The folder where the plugin is installed.
* @param {string[]} [disabledPlugins = []] - An array of disabled plugins.
* @param {string} [headlampVersion=""] - The version of Headlamp for compatibility checking.
* @param {null | ProgressCallback} [progressCallback=null] - Optional callback for progress updates.
* @param {AbortSignal} [signal=null] - Optional AbortSignal for cancellation.
Expand All @@ -127,11 +134,16 @@ export class PluginManager {
static async update(
pluginName,
destinationFolder = defaultPluginsDir(),
disabledPlugins: string[] = [],
headlampVersion = '',
progressCallback: null | ProgressCallback = null,
signal: AbortSignal | null = null
): Promise<void> {
try {
if (disabledPlugins.includes(pluginName)) {
throw new Error(`Plugin ${pluginName} is disabled`);
}

// @todo: should list call take progressCallback?
const installedPlugins = PluginManager.list(destinationFolder);
if (!installedPlugins) {
Expand Down Expand Up @@ -195,15 +207,21 @@ export class PluginManager {
* Uninstalls a plugin from the specified folder.
* @param {string} name - The name of the plugin to uninstall.
* @param {string} [folder=defaultPluginsDir()] - The folder where the plugin is installed.
* @param {string[]} [disabledPlugins = []] - An array of disabled plugins.
* @param {function} [progressCallback=null] - Optional callback for progress updates.
* @returns {void}
*/
static uninstall(
name,
folder = defaultPluginsDir(),
disabledPlugins: string[] = [],
progressCallback: null | ProgressCallback = null
) {
try {
if (disabledPlugins.includes(name)) {
throw new Error(`Plugin ${name} is disabled`);
}

// @todo: should list call take progressCallback?
const installedPlugins = PluginManager.list(folder);
if (!installedPlugins) {
Expand Down Expand Up @@ -239,13 +257,20 @@ export class PluginManager {
/**
* Lists all valid plugins in the specified folder.
* @param {string} [folder=defaultPluginsDir()] - The folder to list plugins from.
* @param {string[]} [disabledPlugins = []] - An array of disabled plugins.
* @param {function} [progressCallback=null] - Optional callback for progress updates.
* @returns {Array<object>} An array of objects representing valid plugins.
*/
static list(folder = defaultPluginsDir(), progressCallback: null | ProgressCallback = null) {
static list(
folder = defaultPluginsDir(),
disabledPlugins: string[] = [],
progressCallback: null | ProgressCallback = null
) {
try {
const pluginsData: PluginData[] = [];

const disableAllPlugins = disabledPlugins.length === 0;

// Read all entries in the specified folder
const entries = fs.readdirSync(folder, { withFileTypes: true });

Expand All @@ -260,7 +285,13 @@ export class PluginManager {
// Read package.json to get the plugin name and version
const packageJsonPath = path.join(pluginDir, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const pluginName = packageJson.name || pluginFolder.name;
const pluginName: string = packageJson.name || pluginFolder.name;

if (disableAllPlugins || disabledPlugins.includes(pluginName)) {
console.log(`Plugin ${pluginName} is disabled`);
continue;
}

const pluginTitle = packageJson.artifacthub.title;
const pluginVersion = packageJson.version || null;
const artifacthubURL = packageJson.artifacthub ? packageJson.artifacthub.url : null;
Expand Down
10 changes: 8 additions & 2 deletions backend/cmd/headlamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type HeadlampConfig struct {
staticDir string
pluginDir string
staticPluginDir string
disabledPlugins string
oidcClientID string
oidcClientSecret string
oidcIdpIssuerURL string
Expand Down Expand Up @@ -316,13 +317,18 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
logger.Log(logger.LevelInfo, nil, nil, "Helm support: "+fmt.Sprint(config.enableHelm))
logger.Log(logger.LevelInfo, nil, nil, "Proxy URLs: "+fmt.Sprint(config.proxyURLs))

plugins.PopulatePluginsCache(config.staticPluginDir, config.pluginDir, config.cache)
plugins.PopulatePluginsCache(config.staticPluginDir, config.pluginDir, config.cache, config.disabledPlugins)

if !config.useInCluster {
// in-cluster mode is unlikely to want reloading plugins.
pluginEventChan := make(chan string)
go plugins.Watch(config.pluginDir, pluginEventChan)
go plugins.HandlePluginEvents(config.staticPluginDir, config.pluginDir, pluginEventChan, config.cache)
go plugins.HandlePluginEvents(config.staticPluginDir,
config.pluginDir,
pluginEventChan,
config.cache,
config.disabledPlugins,
)
// in-cluster mode is unlikely to want reloading kubeconfig.
go kubeconfig.LoadAndWatchFiles(config.kubeConfigStore, kubeConfigPath, kubeconfig.KubeConfig)
}
Expand Down
1 change: 1 addition & 0 deletions backend/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func main() {
staticDir: conf.StaticDir,
insecure: conf.InsecureSsl,
pluginDir: conf.PluginsDir,
disabledPlugins: conf.DisabledPlugins,
oidcClientID: conf.OidcClientID,
oidcClientSecret: conf.OidcClientSecret,
oidcIdpIssuerURL: conf.OidcIdpIssuerURL,
Expand Down
23 changes: 15 additions & 8 deletions backend/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package config

import (
"errors"
"flag"
"fmt"
"io/fs"
"os"
Expand All @@ -11,9 +10,11 @@ import (
"runtime"
"strings"

pflagProvider "github.com/knadh/koanf/providers/posflag"
flag "github.com/spf13/pflag"

"github.com/headlamp-k8s/headlamp/backend/pkg/logger"
"github.com/knadh/koanf"
"github.com/knadh/koanf/providers/basicflag"
"github.com/knadh/koanf/providers/env"
)

Expand All @@ -35,6 +36,7 @@ type Config struct {
OidcClientSecret string `koanf:"oidc-client-secret"`
OidcIdpIssuerURL string `koanf:"oidc-idp-issuer-url"`
OidcScopes string `koanf:"oidc-scopes"`
DisabledPlugins string `koanf:"disable-plugins"`
}

func (c *Config) Validate() error {
Expand Down Expand Up @@ -62,7 +64,7 @@ func (c *Config) Validate() error {
func Parse(args []string) (*Config, error) {
var config Config

f := flagset()
f := flagset(&config)

k := koanf.New(".")

Expand All @@ -73,7 +75,7 @@ func Parse(args []string) (*Config, error) {
}

// First Load default args from flags
if err := k.Load(basicflag.Provider(f, "."), nil); err != nil {
if err := k.Load(pflagProvider.Provider(f, ".", k), nil); err != nil {
logger.Log(logger.LevelError, nil, err, "loading default config from flags")

return nil, fmt.Errorf("error loading default config from flags: %w", err)
Expand All @@ -96,7 +98,7 @@ func Parse(args []string) (*Config, error) {
}

// Load only the flags that were set
if err := k.Load(basicflag.ProviderWithValue(f, ".", func(key string, value string) (string, interface{}) {
if err := k.Load(pflagProvider.ProviderWithValue(f, ".", k, func(key string, value string) (string, interface{}) {
flagSet := false
f.Visit(func(f *flag.Flag) {
if f.Name == key {
Expand Down Expand Up @@ -146,10 +148,10 @@ func Parse(args []string) (*Config, error) {
return &config, nil
}

func flagset() *flag.FlagSet {
func flagset(c *Config) *flag.FlagSet {
f := flag.NewFlagSet("config", flag.ContinueOnError)

f.Bool("in-cluster", false, "Set when running from a k8s cluster")
f.BoolP("in-cluster", "i", false, "Set when running from a k8s cluster")
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.")
Expand All @@ -161,12 +163,17 @@ func flagset() *flag.FlagSet {
f.Uint("port", defaultPort, "Port to listen from")
f.String("proxy-urls", "", "Allow proxy requests to specified URLs")

f.String("oidc-client-id", "", "ClientID for OIDC")
f.StringP("oidc-client-id", "o", "", "ClientID for OIDC")
f.String("oidc-client-secret", "", "ClientSecret for OIDC")
f.String("oidc-idp-issuer-url", "", "Identity provider issuer URL for OIDC")
f.String("oidc-scopes", "profile,email",
"A comma separated list of scopes needed from the OIDC provider")

f.StringVar(&c.DisabledPlugins, "disable-plugins", "",
"List of plugin names to disable, or empty to disable all plugins")

f.Lookup("disable-plugins").NoOptDefVal = "all"

return f
}

Expand Down
2 changes: 1 addition & 1 deletion backend/pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestParse(t *testing.T) {
os.Setenv("HEADLAMP_CONFIG_OIDC_CLIENT_SECRET", "superSecretBotsStayAwayPlease")
defer os.Unsetenv("HEADLAMP_CONFIG_OIDC_CLIENT_SECRET")
args := []string{
"go run ./cmd", "-in-cluster",
"go run ./cmd", "--in-cluster",
}
conf, err := config.Parse(args)
require.NoError(t, err)
Expand Down
59 changes: 52 additions & 7 deletions backend/pkg/plugins/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,43 @@ func periodicallyWatchSubfolders(watcher *fsnotify.Watcher, path string, interva
}
}

// GetDisabledPlugins returns a list of disabled plugins and a boolean indicating if all plugins are disabled.
func GetDisabledPlugins(disablePluginsFlag string) ([]string, bool) {
// If the flag is an empty string, all plugins are disabled.
if disablePluginsFlag == "all" {
return nil, true
}

// Parse the list of plugins to disable.
disabledPlugins := strings.Split(disablePluginsFlag, ",")
for i := range disabledPlugins {
disabledPlugins[i] = strings.TrimSpace(disabledPlugins[i])
}

return disabledPlugins, false
}

// GeneratePluginPaths takes the staticPluginDir and pluginDir and returns a list of plugin paths.
func GeneratePluginPaths(staticPluginDir string, pluginDir string) ([]string, error) {
func GeneratePluginPaths(staticPluginDir string, pluginDir string, disabledPluginsFlag string) ([]string, error) {
// Get the list of disabled plugins and whether all plugins are disabled.
disabledPlugins, disableAll := GetDisabledPlugins(disabledPluginsFlag)

if disableAll {
return []string{}, nil
}

var pluginListURLStatic []string

if staticPluginDir != "" {
var err error

pluginListURLStatic, err = pluginBasePathListForDir(staticPluginDir, "static-plugins")
pluginListURLStatic, err = pluginBasePathListForDir(staticPluginDir, "static-plugins", disabledPlugins)
if err != nil {
return nil, err
}
}

pluginListURL, err := pluginBasePathListForDir(pluginDir, "plugins")
pluginListURL, err := pluginBasePathListForDir(pluginDir, "plugins", disabledPlugins)
if err != nil {
return nil, err
}
Expand All @@ -104,7 +127,7 @@ func GeneratePluginPaths(staticPluginDir string, pluginDir string) ([]string, er
}

// pluginBasePathListForDir returns a list of valid plugin paths for the given directory.
func pluginBasePathListForDir(pluginDir string, baseURL string) ([]string, error) {
func pluginBasePathListForDir(pluginDir string, baseURL string, disabledPlugins []string) ([]string, error) {
files, err := os.ReadDir(pluginDir)
if err != nil && !os.IsNotExist(err) {
logger.Log(logger.LevelError, map[string]string{"pluginDir": pluginDir},
Expand All @@ -116,6 +139,12 @@ func pluginBasePathListForDir(pluginDir string, baseURL string) ([]string, error
pluginListURLs := make([]string, 0, len(files))

for _, f := range files {
if disabledPlugins != nil && utils.Contains(disabledPlugins, f.Name()) {
logger.Log(logger.LevelInfo, map[string]string{"plugin": f.Name()},
nil, "Not including plugin path, plugin is disabled")
continue
}

if !f.IsDir() {
pluginPath := filepath.Join(pluginDir, f.Name())
logger.Log(logger.LevelInfo, map[string]string{"pluginPath": pluginPath},
Expand Down Expand Up @@ -154,6 +183,7 @@ func pluginBasePathListForDir(pluginDir string, baseURL string) ([]string, error
// and plugin refresh key in the cache.
func HandlePluginEvents(staticPluginDir, pluginDir string,
notify <-chan string, cache cache.Cache[interface{}],
disabledPluginsFlag string,
) {
for range notify {
// set the plugin refresh key to true
Expand All @@ -163,7 +193,7 @@ func HandlePluginEvents(staticPluginDir, pluginDir string,
}

// generate the plugin list
pluginList, err := GeneratePluginPaths(staticPluginDir, pluginDir)
pluginList, err := GeneratePluginPaths(staticPluginDir, pluginDir, disabledPluginsFlag)
if err != nil && !os.IsNotExist(err) {
logger.Log(logger.LevelError, nil, err, "generating plugins path")
}
Expand All @@ -176,7 +206,22 @@ func HandlePluginEvents(staticPluginDir, pluginDir string,
}

// PopulatePluginsCache populates the plugin list and plugin refresh key in the cache.
func PopulatePluginsCache(staticPluginDir, pluginDir string, cache cache.Cache[interface{}]) {
func PopulatePluginsCache(staticPluginDir, pluginDir string,
cache cache.Cache[interface{}], disabledPluginsFlag string,
) {
_, disableAll := GetDisabledPlugins(disabledPluginsFlag)

if disableAll {
logger.Log(logger.LevelInfo, nil, nil, "All plugins are disabled")
// Update cache to reflect that all plugins are disabled.
err := cache.Set(context.Background(), PluginListKey, []string{})
if err != nil {
logger.Log(logger.LevelError, nil, err, "setting empty plugin list in cache")
}

return
}

// set the plugin refresh key to false
err := cache.Set(context.Background(), PluginRefreshKey, false)
if err != nil {
Expand All @@ -185,7 +230,7 @@ func PopulatePluginsCache(staticPluginDir, pluginDir string, cache cache.Cache[i
}

// generate the plugin list
pluginList, err := GeneratePluginPaths(staticPluginDir, pluginDir)
pluginList, err := GeneratePluginPaths(staticPluginDir, pluginDir, disabledPluginsFlag)
if err != nil && !os.IsNotExist(err) {
logger.Log(logger.LevelError,
map[string]string{"staticPluginDir": staticPluginDir, "pluginDir": pluginDir},
Expand Down
Loading

0 comments on commit 845968d

Please sign in to comment.