mirror of
https://github.com/grafana/grafana.git
synced 2025-12-18 22:16:21 -05:00
feat: map GF_INSTALL_PLUGINS to use preinstall plugins feature (#105145)
* feat: map GF_INSTALL_PLUGINS to use preinstall plugins feature * ref: process GF_INSTALL_PLUGINS as sync install * fix: check GF_INSTALL_PLUGINS_FORCE in the docker run * ref: use preinstall_sync for GF_INSTALL_PLUGINS * ref: logs and deprecation msg for GF_INSTALL_PLUGINS * chore: deprecated log formatting
This commit is contained in:
parent
267e3ffb7c
commit
e05ccb822a
5 changed files with 225 additions and 21 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue