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 Nov 15, 2024
1 parent d24fc9f commit c15fdc5
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 33 deletions.
16 changes: 16 additions & 0 deletions app/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,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 (arg === undefined) {
return 'all';
}
return arg.split(',');
},
},
})
.positional('kubeconfig', {
describe:
Expand Down Expand Up @@ -536,6 +546,12 @@ async function startServer(flags: string[] = []): Promise<ChildProcessWithoutNul
if (!!proxyUrls && proxyUrls.length > 0) {
serverArgs = serverArgs.concat(['--proxy-urls', proxyUrls.join(',')]);
}
const disablePluginsFlag = args['disable-plugins'];
if (disablePluginsFlag.length === 0) {
serverArgs.push('--disable-plugins');
} else {
serverArgs = serverArgs.concat(['--disable-plugins', disablePluginsFlag.join(',')]);
}

const bundledPlugins = path.join(process.resourcesPath, '.plugins');

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
disablePlugins string
oidcClientID string
oidcClientSecret string
oidcIdpIssuerURL string
Expand Down Expand Up @@ -344,13 +345,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.disablePlugins)

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.disablePlugins,
)
// in-cluster mode is unlikely to want reloading kubeconfig.
go kubeconfig.LoadAndWatchFiles(config.kubeConfigStore, kubeConfigPath, kubeconfig.KubeConfig)
}
Expand Down
3 changes: 2 additions & 1 deletion backend/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func main() {
os.Exit(1)
}

if err := plugins.ListPlugins(conf.StaticDir, conf.PluginsDir); err != nil {
if err := plugins.ListPlugins(conf.StaticDir, conf.PluginsDir, conf.DisablePlugins); err != nil {
logger.Log(logger.LevelError, nil, err, "listing plugins")
}

Expand All @@ -44,6 +44,7 @@ func main() {
staticDir: conf.StaticDir,
insecure: conf.InsecureSsl,
pluginDir: conf.PluginsDir,
disablePlugins: conf.DisablePlugins,
oidcClientID: conf.OidcClientID,
oidcClientSecret: conf.OidcClientSecret,
oidcIdpIssuerURL: conf.OidcIdpIssuerURL,
Expand Down
19 changes: 13 additions & 6 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"`
DisablePlugins string `koanf:"disable-plugins"`
}

func (c *Config) Validate() error {
Expand Down Expand Up @@ -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 @@ -149,7 +151,7 @@ func Parse(args []string) (*Config, error) {
func flagset() *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.String("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
Loading

0 comments on commit c15fdc5

Please sign in to comment.