diff --git a/docs/sources/setup-grafana/start-restart-grafana.md b/docs/sources/setup-grafana/start-restart-grafana.md index 0cba9899590..f0c4e570b87 100644 --- a/docs/sources/setup-grafana/start-restart-grafana.md +++ b/docs/sources/setup-grafana/start-restart-grafana.md @@ -128,7 +128,7 @@ services: restart: unless-stopped environment: - TERM=linux - - GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-polystat-panel + - GF_PLUGINS_PREINSTALL=grafana-clock-panel,grafana-polystat-panel ports: - '3000:3000' volumes: diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh index ac1f9ea45a6..a4c91b49379 100755 --- a/packaging/docker/run.sh +++ b/packaging/docker/run.sh @@ -63,18 +63,21 @@ done export HOME="$GF_PATHS_HOME" if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then - OLDIFS=$IFS - IFS=',' - for plugin in ${GF_INSTALL_PLUGINS}; do - IFS=$OLDIFS - if [[ $plugin =~ .*\;.* ]]; then - pluginUrl=$(echo "$plugin" | cut -d';' -f 1) - pluginInstallFolder=$(echo "$plugin" | cut -d';' -f 2) - grafana cli --pluginUrl ${pluginUrl} --pluginsDir "${GF_PATHS_PLUGINS}" plugins install "${pluginInstallFolder}" - else - grafana cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin} - fi - done + >&2 echo "\033[0;33mWARN\033[0m: GF_INSTALL_PLUGINS is deprecated. Use GF_PLUGINS_PREINSTALL or GF_PLUGINS_PREINSTALL_SYNC instead. Checkout the documentation for more info." + if [ "${GF_INSTALL_PLUGINS_FORCE}" = "true" ]; then + OLDIFS=$IFS + IFS=',' + for plugin in ${GF_INSTALL_PLUGINS}; do + IFS=$OLDIFS + if [[ $plugin =~ .*\;.* ]]; then + pluginUrl=$(echo "$plugin" | cut -d';' -f 1) + pluginInstallFolder=$(echo "$plugin" | cut -d';' -f 2) + grafana cli --pluginUrl ${pluginUrl} --pluginsDir "${GF_PATHS_PLUGINS}" plugins install "${pluginInstallFolder}" + else + grafana cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin} + fi + done + fi fi exec grafana server \ diff --git a/pkg/setting/setting_plugins.go b/pkg/setting/setting_plugins.go index 3fb980c38da..005950e9611 100644 --- a/pkg/setting/setting_plugins.go +++ b/pkg/setting/setting_plugins.go @@ -1,6 +1,8 @@ package setting import ( + "os" + "regexp" "strings" "gopkg.in/ini.v1" @@ -34,13 +36,62 @@ func extractPluginSettings(sections []*ini.Section) PluginSettings { var ( defaultPreinstallPlugins = map[string]InstallPlugin{ // Default preinstalled plugins - "grafana-lokiexplore-app": {"grafana-lokiexplore-app", "", ""}, - "grafana-pyroscope-app": {"grafana-pyroscope-app", "", ""}, - "grafana-exploretraces-app": {"grafana-exploretraces-app", "", ""}, - "grafana-metricsdrilldown-app": {"grafana-metricsdrilldown-app", "", ""}, + "grafana-lokiexplore-app": {ID: "grafana-lokiexplore-app"}, + "grafana-pyroscope-app": {ID: "grafana-pyroscope-app"}, + "grafana-exploretraces-app": {ID: "grafana-exploretraces-app"}, + "grafana-metricsdrilldown-app": {ID: "grafana-metricsdrilldown-app"}, } ) +func (cfg *Cfg) migrateInstallPluginsToPreinstallPluginsSync(rawInstallPlugins, installPluginsForce string, preinstallPluginsSync map[string]InstallPlugin) { + if strings.ToLower(installPluginsForce) == "true" || rawInstallPlugins == "" { + cfg.Logger.Debug("GF_INSTALL_PLUGINS_FORCE is set to true, skipping migration of GF_INSTALL_PLUGINS to GF_PLUGINS_PREINSTALL_SYNC") + return + } + installPluginsEntries := strings.Split(rawInstallPlugins, ",") + + // Format 1: ID only (e.g., "grafana-clock-panel") + // Format 2: ID with version (e.g., "grafana-clock-panel 1.0.1") + // Format 3: URL with folder (e.g., "https://grafana.com/api/plugins/grafana-clock-panel/versions/latest/download;grafana-clock-panel") + pluginRegex := regexp.MustCompile(`(?:([^;]+);)?([^;\s]+)(?:\s+(.+))?`) + for _, entry := range installPluginsEntries { + trimmedEntry := strings.TrimSpace(entry) + if trimmedEntry == "" { + continue + } + + matches := pluginRegex.FindStringSubmatch(trimmedEntry) + + if matches == nil { + cfg.Logger.Debug("No match found for entry: ", trimmedEntry) + continue + } + + url := "" + if len(matches) > 1 { + url = strings.TrimSpace(matches[1]) + } + + id := "" + if len(matches) > 2 { + id = strings.TrimSpace(matches[2]) + } + if _, exists := preinstallPluginsSync[id]; exists { + continue + } + + version := "" + if len(matches) > 3 { + version = strings.TrimSpace(matches[3]) + } + if id != "" { + preinstallPluginsSync[id] = InstallPlugin{ID: id, Version: version, URL: url} + } else { + cfg.Logger.Debug("No ID found for entry: ", trimmedEntry, "matches: ", matches) + } + } +} + func (cfg *Cfg) processPreinstallPlugins(rawInstallPlugins []string, preinstallPlugins map[string]InstallPlugin) { // Add the plugins defined in the configuration for _, plugin := range rawInstallPlugins { @@ -88,6 +139,7 @@ func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error { rawInstallPluginsSync := util.SplitString(pluginsSection.Key("preinstall_sync").MustString("")) preinstallPluginsSync := make(map[string]InstallPlugin) cfg.processPreinstallPlugins(rawInstallPluginsSync, preinstallPluginsSync) + cfg.migrateInstallPluginsToPreinstallPluginsSync(os.Getenv("GF_INSTALL_PLUGINS"), os.Getenv("GF_INSTALL_PLUGINS_FORCE"), preinstallPluginsSync) // Remove from the list the plugins that have been disabled for _, disabledPlugin := range cfg.DisablePlugins { delete(preinstallPluginsAsync, disabledPlugin) diff --git a/pkg/setting/setting_plugins_test.go b/pkg/setting/setting_plugins_test.go index 1af9c95270f..74425c95a0e 100644 --- a/pkg/setting/setting_plugins_test.go +++ b/pkg/setting/setting_plugins_test.go @@ -172,12 +172,12 @@ func Test_readPluginSettings(t *testing.T) { { name: "should add the default preinstalled plugin and the one defined", rawInput: "plugin1", - expected: append(defaultPreinstallPluginsList, InstallPlugin{"plugin1", "", ""}), + expected: append(defaultPreinstallPluginsList, InstallPlugin{ID: "plugin1", Version: "", URL: ""}), }, { name: "should add the default preinstalled plugin and the one defined with version", rawInput: "plugin1@1.0.0", - expected: append(defaultPreinstallPluginsList, InstallPlugin{"plugin1", "1.0.0", ""}), + expected: append(defaultPreinstallPluginsList, InstallPlugin{ID: "plugin1", Version: "1.0.0", URL: ""}), }, { name: "it should remove the disabled plugin", @@ -207,12 +207,12 @@ func Test_readPluginSettings(t *testing.T) { { name: "should parse a plugin with version and URL", rawInput: "plugin1@1.0.1@https://example.com/plugin1.tar.gz", - expected: append(defaultPreinstallPluginsList, InstallPlugin{"plugin1", "1.0.1", "https://example.com/plugin1.tar.gz"}), + expected: append(defaultPreinstallPluginsList, InstallPlugin{ID: "plugin1", Version: "1.0.1", URL: "https://example.com/plugin1.tar.gz"}), }, { name: "should parse a plugin with URL", rawInput: "plugin1@@https://example.com/plugin1.tar.gz", - expected: append(defaultPreinstallPluginsList, InstallPlugin{"plugin1", "", "https://example.com/plugin1.tar.gz"}), + expected: append(defaultPreinstallPluginsList, InstallPlugin{ID: "plugin1", Version: "", URL: "https://example.com/plugin1.tar.gz"}), }, { name: "when preinstall_async is false, should add all plugins to preinstall_sync", @@ -272,3 +272,140 @@ func Test_readPluginSettings(t *testing.T) { } }) } + +func Test_migrateInstallPluginsToPreinstallPluginsSync(t *testing.T) { + tests := []struct { + name string + installPluginsVal string + installPluginsForce string + preinstallPlugins map[string]InstallPlugin + expectedPlugins map[string]InstallPlugin + }{ + { + name: "should return empty map when GF_INSTALL_PLUGINS is not set", + installPluginsVal: "", + preinstallPlugins: map[string]InstallPlugin{}, + expectedPlugins: map[string]InstallPlugin{}, + }, + { + name: "should parse URL with folder format", + installPluginsVal: "https://grafana.com/grafana/plugins/grafana-piechart-panel/;grafana-piechart-panel", + preinstallPlugins: map[string]InstallPlugin{}, + expectedPlugins: map[string]InstallPlugin{ + "grafana-piechart-panel": { + ID: "grafana-piechart-panel", + Version: "", + URL: "https://grafana.com/grafana/plugins/grafana-piechart-panel/", + }, + }, + }, + { + name: "should parse mixed formats", + installPluginsVal: "https://github.com/VolkovLabs/business-links/releases/download/v1.2.1/volkovlabs-links-panel-1.2.1.zip;volkovlabs-links-panel,marcusolsson-static-datasource,volkovlabs-variable-panel", + preinstallPlugins: map[string]InstallPlugin{}, + expectedPlugins: map[string]InstallPlugin{ + "volkovlabs-links-panel": { + ID: "volkovlabs-links-panel", + Version: "", + URL: "https://github.com/VolkovLabs/business-links/releases/download/v1.2.1/volkovlabs-links-panel-1.2.1.zip", + }, + "marcusolsson-static-datasource": { + ID: "marcusolsson-static-datasource", + Version: "", + URL: "", + }, + "volkovlabs-variable-panel": { + ID: "volkovlabs-variable-panel", + Version: "", + URL: "", + }, + }, + }, + { + name: "should parse ID with version format", + installPluginsVal: "volkovlabs-links-panel 1.2.1,marcusolsson-static-datasource 1.0.0,volkovlabs-variable-panel", + preinstallPlugins: map[string]InstallPlugin{}, + expectedPlugins: map[string]InstallPlugin{ + "volkovlabs-links-panel": { + ID: "volkovlabs-links-panel", + Version: "1.2.1", + URL: "", + }, + "marcusolsson-static-datasource": { + ID: "marcusolsson-static-datasource", + Version: "1.0.0", + URL: "", + }, + "volkovlabs-variable-panel": { + ID: "volkovlabs-variable-panel", + Version: "", + URL: "", + }, + }, + }, + { + name: "should return empty map when GF_INSTALL_PLUGINS_FORCE is true", + installPluginsVal: "grafana-piechart-panel", + installPluginsForce: "true", + preinstallPlugins: map[string]InstallPlugin{}, + expectedPlugins: map[string]InstallPlugin{}, + }, + { + name: "should skip plugins that are already configured", + installPluginsVal: "plugin1 1.0.0,plugin2,plugin3", + preinstallPlugins: map[string]InstallPlugin{ + "plugin1": {ID: "plugin1", Version: "1.0.1"}, + "plugin3": {ID: "plugin3"}, + }, + expectedPlugins: map[string]InstallPlugin{ + "plugin2": { + ID: "plugin2", + }, + "plugin3": { + ID: "plugin3", + }, + "plugin1": { + ID: "plugin1", + Version: "1.0.1", + }, + }, + }, + { + name: "should trim the space in the input", + installPluginsVal: " plugin1 1.0.0, plugin2, plugin3 ", + preinstallPlugins: map[string]InstallPlugin{}, + expectedPlugins: map[string]InstallPlugin{ + "plugin2": { + ID: "plugin2", + }, + "plugin3": { + ID: "plugin3", + }, + "plugin1": { + ID: "plugin1", + Version: "1.0.0", + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := NewCfg() + + cfg.migrateInstallPluginsToPreinstallPluginsSync(tc.installPluginsVal, tc.installPluginsForce, tc.preinstallPlugins) + assert.Equal(t, len(tc.expectedPlugins), len(tc.preinstallPlugins), "Number of plugins doesn't match") + + // Check each expected plugin exists with correct values + for id, expectedPlugin := range tc.expectedPlugins { + actualPlugin, exists := tc.preinstallPlugins[id] + assert.True(t, exists, "Expected plugin %s not found", id) + if exists { + assert.Equal(t, expectedPlugin.ID, actualPlugin.ID, "Plugin ID mismatch for %s", id) + assert.Equal(t, expectedPlugin.Version, actualPlugin.Version, "Plugin version mismatch for %s", id) + assert.Equal(t, expectedPlugin.URL, actualPlugin.URL, "Plugin URL mismatch for %s", id) + } + } + }) + } +} diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 9a434a7e2ff..c40f761cad4 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -78,6 +78,18 @@ func TestLoadingSettings(t *testing.T) { require.Equal(t, filepath.Join(cfg.DataPath, "log"), cfg.LogsPath) }) + t.Run("Should be able to override via plugins.preinstall with GF_INSTALL_PLUGINS env var when GF_PLUGINS_PREINSTALL and cfg.plugins.preinstall are not set", func(t *testing.T) { + t.Setenv("GF_INSTALL_PLUGINS", "https://grafana.com/grafana/plugins/grafana-piechart-panel/;grafana-piechart-panel") + + cfg := NewCfg() + err := cfg.Load(CommandLineArgs{HomePath: "../../"}) + require.Nil(t, err) + + require.Equal(t, filepath.Join(cfg.HomePath, "data"), cfg.DataPath) + require.Equal(t, filepath.Join(cfg.DataPath, "log"), cfg.LogsPath) + require.Equal(t, cfg.PreinstallPluginsSync, []InstallPlugin{{ID: "grafana-piechart-panel", Version: "", URL: "https://grafana.com/grafana/plugins/grafana-piechart-panel/"}}) + }) + t.Run("Should be able to expand parameter from environment variables", func(t *testing.T) { t.Setenv("DEFAULT_IDP_URL", "grafana.com") t.Setenv("GF_AUTH_GENERIC_OAUTH_AUTH_URL", "${DEFAULT_IDP_URL}/auth")