Commit cc95754e by Marcus Efraimsson Committed by GitHub

Provisioning: Adds support for enabling app plugins (#25649)

Adds support for enabling app plugins using provisioning. 

Ref #11409

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
parent 602dd1e2
# # config file version
apiVersion: 1
# apps:
# - type: grafana-example-app
# org_name: Main Org.
# disabled: true
# - type: raintank-worldping-app
# org_id: 1
# jsonData:
# apiKey: "API KEY"
...@@ -65,7 +65,7 @@ Currently we do not provide any scripts/manifests for configuring Grafana. Rathe ...@@ -65,7 +65,7 @@ Currently we do not provide any scripts/manifests for configuring Grafana. Rathe
> This feature is available from v5.0 > This feature is available from v5.0
It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`provisioning/datasources`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `deleteDatasources`. Grafana will delete datasources listed in `deleteDatasources` before inserting/updating those in the `datasource` list. You can manage data sources in Grafana by adding one or more YAML config files in the [`provisioning/datasources`]({{< relref "../installation/configuration/#provisioning" >}}) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the data source already exists, then Grafana updates it to match the configuration file. The config file can also contain a list of data sources that should be deleted. That list is called `deleteDatasources`. Grafana deletes data sources listed in `deleteDatasources` before inserting or updating those in the `datasource` list.
### Running Multiple Grafana Instances ### Running Multiple Grafana Instances
...@@ -208,9 +208,39 @@ datasources: ...@@ -208,9 +208,39 @@ datasources:
httpHeaderValue2: 'Bearer XXXXXXXXX' httpHeaderValue2: 'Bearer XXXXXXXXX'
``` ```
## Plugins
> This feature is available from v7.1
You can manage plugins in Grafana by adding one or more YAML config files in the [`provisioning/plugins`]({{< relref "../installation/configuration/#provisioning" >}}) directory. Each config file can contain a list of `apps` that will be updated during start up. Grafana updates each app to match the configuration file.
### Example plugin configuration file
```yaml
apiVersion: 1
apps:
# <string> the type of app, plugin identifier. Required
- type: raintank-worldping-app
# <int> Org ID. Default to 1, unless org_name is specified
org_id: 1
# <string> Org name. Overrides org_id unless org_id not specified
org_name: Main Org.
# <bool> disable the app. Default to false.
disabled: false
# <map> fields that will be converted to json and stored in jsonData. Custom per app.
jsonData:
# key/value pairs of string to object
key: value
# <map> fields that will be converted to json, encrypted and stored in secureJsonData. Custom per app.
secureJsonData:
# key/value pairs of string to string
key: value
```
## Dashboards ## Dashboards
It's possible to manage dashboards in Grafana by adding one or more yaml config files in the [`provisioning/dashboards`]({{< relref "../installation/configuration.md" >}}) directory. Each config file can contain a list of `dashboards providers` that will load dashboards into Grafana from the local filesystem. You can manage dashboards in Grafana by adding one or more YAML config files in the [`provisioning/dashboards`]({{< relref "../installation/configuration.md" >}}) directory. Each config file can contain a list of `dashboards providers` that load dashboards into Grafana from the local filesystem.
The dashboard provider config file looks somewhat like this: The dashboard provider config file looks somewhat like this:
......
...@@ -465,6 +465,8 @@ Content-Type: application/json ...@@ -465,6 +465,8 @@ Content-Type: application/json
`POST /api/admin/provisioning/datasources/reload` `POST /api/admin/provisioning/datasources/reload`
`POST /api/admin/provisioning/plugins/reload`
`POST /api/admin/provisioning/notifications/reload` `POST /api/admin/provisioning/notifications/reload`
Reloads the provisioning config files for specified type and provision entities again. It won't return Reloads the provisioning config files for specified type and provision entities again. It won't return
......
...@@ -42,6 +42,11 @@ case "$1" in ...@@ -42,6 +42,11 @@ case "$1" in
cp /usr/share/grafana/conf/provisioning/notifiers/sample.yaml $PROVISIONING_CFG_DIR/notifiers/sample.yaml cp /usr/share/grafana/conf/provisioning/notifiers/sample.yaml $PROVISIONING_CFG_DIR/notifiers/sample.yaml
fi fi
if [ ! -d $PROVISIONING_CFG_DIR/plugins ]; then
mkdir -p $PROVISIONING_CFG_DIR/plugins
cp /usr/share/grafana/conf/provisioning/plugins/sample.yaml $PROVISIONING_CFG_DIR/plugins/sample.yaml
fi
# configuration files should not be modifiable by grafana user, as this can be a security issue # configuration files should not be modifiable by grafana user, as this can be a security issue
chown -Rh root:$GRAFANA_GROUP /etc/grafana/* chown -Rh root:$GRAFANA_GROUP /etc/grafana/*
chmod 755 /etc/grafana chmod 755 /etc/grafana
......
...@@ -56,6 +56,11 @@ if [ $1 -eq 1 ] ; then ...@@ -56,6 +56,11 @@ if [ $1 -eq 1 ] ; then
cp /usr/share/grafana/conf/provisioning/notifiers/sample.yaml $PROVISIONING_CFG_DIR/notifiers/sample.yaml cp /usr/share/grafana/conf/provisioning/notifiers/sample.yaml $PROVISIONING_CFG_DIR/notifiers/sample.yaml
fi fi
if [ ! -d $PROVISIONING_CFG_DIR/plugins ]; then
mkdir -p $PROVISIONING_CFG_DIR/plugins
cp /usr/share/grafana/conf/provisioning/plugins/sample.yaml $PROVISIONING_CFG_DIR/plugins/sample.yaml
fi
# Set user permissions on /var/log/grafana, /var/lib/grafana # Set user permissions on /var/log/grafana, /var/lib/grafana
mkdir -p /var/log/grafana /var/lib/grafana mkdir -p /var/log/grafana /var/lib/grafana
chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana
......
...@@ -2,6 +2,7 @@ package api ...@@ -2,6 +2,7 @@ package api
import ( import (
"context" "context"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
) )
...@@ -21,6 +22,14 @@ func (server *HTTPServer) AdminProvisioningReloadDatasources(c *models.ReqContex ...@@ -21,6 +22,14 @@ func (server *HTTPServer) AdminProvisioningReloadDatasources(c *models.ReqContex
return Success("Datasources config reloaded") return Success("Datasources config reloaded")
} }
func (server *HTTPServer) AdminProvisioningReloadPlugins(c *models.ReqContext) Response {
err := server.ProvisioningService.ProvisionPlugins()
if err != nil {
return Error(500, "Failed to reload plugins config", err)
}
return Success("Plugins config reloaded")
}
func (server *HTTPServer) AdminProvisioningReloadNotifications(c *models.ReqContext) Response { func (server *HTTPServer) AdminProvisioningReloadNotifications(c *models.ReqContext) Response {
err := server.ProvisioningService.ProvisionNotifications() err := server.ProvisioningService.ProvisionNotifications()
if err != nil { if err != nil {
......
...@@ -405,6 +405,7 @@ func (hs *HTTPServer) registerRoutes() { ...@@ -405,6 +405,7 @@ func (hs *HTTPServer) registerRoutes() {
adminRoute.Post("/users/:id/revoke-auth-token", bind(models.RevokeAuthTokenCmd{}), Wrap(hs.AdminRevokeUserAuthToken)) adminRoute.Post("/users/:id/revoke-auth-token", bind(models.RevokeAuthTokenCmd{}), Wrap(hs.AdminRevokeUserAuthToken))
adminRoute.Post("/provisioning/dashboards/reload", Wrap(hs.AdminProvisioningReloadDashboards)) adminRoute.Post("/provisioning/dashboards/reload", Wrap(hs.AdminProvisioningReloadDashboards))
adminRoute.Post("/provisioning/plugins/reload", Wrap(hs.AdminProvisioningReloadPlugins))
adminRoute.Post("/provisioning/datasources/reload", Wrap(hs.AdminProvisioningReloadDatasources)) adminRoute.Post("/provisioning/datasources/reload", Wrap(hs.AdminProvisioningReloadDatasources))
adminRoute.Post("/provisioning/notifications/reload", Wrap(hs.AdminProvisioningReloadNotifications)) adminRoute.Post("/provisioning/notifications/reload", Wrap(hs.AdminProvisioningReloadNotifications))
adminRoute.Post("/ldap/reload", Wrap(hs.ReloadLDAPCfg)) adminRoute.Post("/ldap/reload", Wrap(hs.ReloadLDAPCfg))
......
...@@ -85,3 +85,9 @@ func GetEnabledPlugins(orgId int64) (*EnabledPlugins, error) { ...@@ -85,3 +85,9 @@ func GetEnabledPlugins(orgId int64) (*EnabledPlugins, error) {
return &enabledPlugins, nil return &enabledPlugins, nil
} }
// IsAppInstalled checks if an app plugin with provided plugin ID is installed.
func IsAppInstalled(pluginID string) bool {
_, exists := Apps[pluginID]
return exists
}
package plugins
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"gopkg.in/yaml.v2"
)
type configReader interface {
readConfig(path string) ([]*pluginsAsConfig, error)
}
type configReaderImpl struct {
log log.Logger
}
func newConfigReader(logger log.Logger) configReader {
return &configReaderImpl{log: logger}
}
func (cr *configReaderImpl) readConfig(path string) ([]*pluginsAsConfig, error) {
var apps []*pluginsAsConfig
cr.log.Debug("Looking for plugin provisioning files", "path", path)
files, err := ioutil.ReadDir(path)
if err != nil {
cr.log.Error("Failed to read plugin provisioning files from directory", "path", path, "error", err)
return apps, nil
}
for _, file := range files {
if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") {
cr.log.Debug("Parsing plugin provisioning file", "path", path, "file.Name", file.Name())
app, err := cr.parsePluginConfig(path, file)
if err != nil {
return nil, err
}
if app != nil {
apps = append(apps, app)
}
}
}
cr.log.Debug("Validating plugins")
if err := validateRequiredField(apps); err != nil {
return nil, err
}
checkOrgIDAndOrgName(apps)
err = validatePluginsConfig(apps)
if err != nil {
return nil, err
}
return apps, nil
}
func (cr *configReaderImpl) parsePluginConfig(path string, file os.FileInfo) (*pluginsAsConfig, error) {
filename, err := filepath.Abs(filepath.Join(path, file.Name()))
if err != nil {
return nil, err
}
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
var cfg *pluginsAsConfigV0
err = yaml.Unmarshal(yamlFile, &cfg)
if err != nil {
return nil, err
}
return cfg.mapToPluginsFromConfig(), nil
}
func validateRequiredField(apps []*pluginsAsConfig) error {
for i := range apps {
var errStrings []string
for index, app := range apps[i].Apps {
if app.PluginID == "" {
errStrings = append(
errStrings,
fmt.Sprintf("app item %d in configuration doesn't contain required field type", index+1),
)
}
}
if len(errStrings) != 0 {
return fmt.Errorf(strings.Join(errStrings, "\n"))
}
}
return nil
}
func validatePluginsConfig(apps []*pluginsAsConfig) error {
for i := range apps {
if apps[i].Apps == nil {
continue
}
for _, app := range apps[i].Apps {
if !plugins.IsAppInstalled(app.PluginID) {
return fmt.Errorf("app plugin not installed: %s", app.PluginID)
}
}
}
return nil
}
func checkOrgIDAndOrgName(apps []*pluginsAsConfig) {
for i := range apps {
for _, app := range apps[i].Apps {
if app.OrgID < 1 {
if app.OrgName == "" {
app.OrgID = 1
} else {
app.OrgID = 0
}
}
}
}
}
package plugins
import (
"os"
"testing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/stretchr/testify/require"
)
var (
incorrectSettings = "./testdata/test-configs/incorrect-settings"
brokenYaml = "./testdata/test-configs/broken-yaml"
emptyFolder = "./testdata/test-configs/empty_folder"
unknownApp = "./testdata/test-configs/unknown-app"
correctProperties = "./testdata/test-configs/correct-properties"
)
func TestConfigReader(t *testing.T) {
t.Run("Broken yaml should return error", func(t *testing.T) {
reader := newConfigReader(log.New("test logger"))
_, err := reader.readConfig(brokenYaml)
require.Error(t, err)
})
t.Run("Skip invalid directory", func(t *testing.T) {
cfgProvider := newConfigReader(log.New("test logger"))
cfg, err := cfgProvider.readConfig(emptyFolder)
require.NoError(t, err)
require.Len(t, cfg, 0)
})
t.Run("Unknown app plugin should return error", func(t *testing.T) {
cfgProvider := newConfigReader(log.New("test logger"))
_, err := cfgProvider.readConfig(unknownApp)
require.Error(t, err)
require.Equal(t, "app plugin not installed: nonexisting", err.Error())
})
t.Run("Read incorrect properties", func(t *testing.T) {
cfgProvider := newConfigReader(log.New("test logger"))
_, err := cfgProvider.readConfig(incorrectSettings)
require.Error(t, err)
require.Equal(t, "app item 1 in configuration doesn't contain required field type", err.Error())
})
t.Run("Can read correct properties", func(t *testing.T) {
plugins.Apps = map[string]*plugins.AppPlugin{
"test-plugin": {},
"test-plugin-2": {},
}
err := os.Setenv("ENABLE_PLUGIN_VAR", "test-plugin")
require.NoError(t, err)
t.Cleanup(func() {
_ = os.Unsetenv("ENABLE_PLUGIN_VAR")
})
cfgProvider := newConfigReader(log.New("test logger"))
cfg, err := cfgProvider.readConfig(correctProperties)
require.NoError(t, err)
require.Len(t, cfg, 1)
testCases := []struct {
ExpectedPluginID string
ExpectedOrgID int64
ExpectedOrgName string
ExpectedEnabled bool
}{
{ExpectedPluginID: "test-plugin", ExpectedOrgID: 2, ExpectedOrgName: "", ExpectedEnabled: true},
{ExpectedPluginID: "test-plugin-2", ExpectedOrgID: 3, ExpectedOrgName: "", ExpectedEnabled: false},
{ExpectedPluginID: "test-plugin", ExpectedOrgID: 0, ExpectedOrgName: "Org 3", ExpectedEnabled: true},
{ExpectedPluginID: "test-plugin-2", ExpectedOrgID: 1, ExpectedOrgName: "", ExpectedEnabled: true},
}
for index, tc := range testCases {
app := cfg[0].Apps[index]
require.NotNil(t, app)
require.Equal(t, tc.ExpectedPluginID, app.PluginID)
require.Equal(t, tc.ExpectedOrgID, app.OrgID)
require.Equal(t, tc.ExpectedOrgName, app.OrgName)
require.Equal(t, tc.ExpectedEnabled, app.Enabled)
}
})
}
package plugins
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
)
// Provision scans a directory for provisioning config files
// and provisions the app in those files.
func Provision(configDirectory string) error {
ap := newAppProvisioner(log.New("provisioning.plugins"))
return ap.applyChanges(configDirectory)
}
// PluginProvisioner is responsible for provisioning apps based on
// configuration read by the `configReader`
type PluginProvisioner struct {
log log.Logger
cfgProvider configReader
}
func newAppProvisioner(log log.Logger) PluginProvisioner {
return PluginProvisioner{
log: log,
cfgProvider: newConfigReader(log),
}
}
func (ap *PluginProvisioner) apply(cfg *pluginsAsConfig) error {
for _, app := range cfg.Apps {
if app.OrgID == 0 && app.OrgName != "" {
getOrgQuery := &models.GetOrgByNameQuery{Name: app.OrgName}
if err := bus.Dispatch(getOrgQuery); err != nil {
return err
}
app.OrgID = getOrgQuery.Result.Id
} else if app.OrgID < 0 {
app.OrgID = 1
}
query := &models.GetPluginSettingByIdQuery{OrgId: app.OrgID, PluginId: app.PluginID}
err := bus.Dispatch(query)
if err != nil {
if err != models.ErrPluginSettingNotFound {
return err
}
} else {
app.PluginVersion = query.Result.PluginVersion
app.Pinned = query.Result.Pinned
}
ap.log.Info("Updating app from configuration ", "type", app.PluginID, "enabled", app.Enabled)
cmd := &models.UpdatePluginSettingCmd{
OrgId: app.OrgID,
PluginId: app.PluginID,
Enabled: app.Enabled,
Pinned: app.Pinned,
JsonData: app.JSONData,
SecureJsonData: app.SecureJSONData,
PluginVersion: app.PluginVersion,
}
if err := bus.Dispatch(cmd); err != nil {
return err
}
}
return nil
}
func (ap *PluginProvisioner) applyChanges(configPath string) error {
configs, err := ap.cfgProvider.readConfig(configPath)
if err != nil {
return err
}
for _, cfg := range configs {
if err := ap.apply(cfg); err != nil {
return err
}
}
return nil
}
package plugins
import (
"errors"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/require"
)
func TestPluginProvisioner(t *testing.T) {
t.Run("Should return error when config reader returns error", func(t *testing.T) {
expectedErr := errors.New("test")
reader := &testConfigReader{err: expectedErr}
ap := PluginProvisioner{log: log.New("test"), cfgProvider: reader}
err := ap.applyChanges("")
require.Equal(t, expectedErr, err)
})
t.Run("Should apply configurations", func(t *testing.T) {
bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error {
if query.Name == "Org 4" {
query.Result = &models.Org{Id: 4}
}
return nil
})
bus.AddHandler("test", func(query *models.GetPluginSettingByIdQuery) error {
if query.PluginId == "test-plugin" && query.OrgId == 2 {
query.Result = &models.PluginSetting{
PluginVersion: "2.0.1",
}
return nil
}
return models.ErrPluginSettingNotFound
})
sentCommands := []*models.UpdatePluginSettingCmd{}
bus.AddHandler("test", func(cmd *models.UpdatePluginSettingCmd) error {
sentCommands = append(sentCommands, cmd)
return nil
})
cfg := []*pluginsAsConfig{
{
Apps: []*appFromConfig{
{PluginID: "test-plugin", OrgID: 2, Enabled: true},
{PluginID: "test-plugin-2", OrgID: 3, Enabled: false},
{PluginID: "test-plugin", OrgName: "Org 4", Enabled: true},
{PluginID: "test-plugin-2", OrgID: 1, Enabled: true},
},
},
}
reader := &testConfigReader{result: cfg}
ap := PluginProvisioner{log: log.New("test"), cfgProvider: reader}
err := ap.applyChanges("")
require.NoError(t, err)
require.Len(t, sentCommands, 4)
testCases := []struct {
ExpectedPluginID string
ExpectedOrgID int64
ExpectedEnabled bool
ExpectedPluginVersion string
}{
{ExpectedPluginID: "test-plugin", ExpectedOrgID: 2, ExpectedEnabled: true, ExpectedPluginVersion: "2.0.1"},
{ExpectedPluginID: "test-plugin-2", ExpectedOrgID: 3, ExpectedEnabled: false},
{ExpectedPluginID: "test-plugin", ExpectedOrgID: 4, ExpectedEnabled: true},
{ExpectedPluginID: "test-plugin-2", ExpectedOrgID: 1, ExpectedEnabled: true},
}
for index, tc := range testCases {
cmd := sentCommands[index]
require.NotNil(t, cmd)
require.Equal(t, tc.ExpectedPluginID, cmd.PluginId)
require.Equal(t, tc.ExpectedOrgID, cmd.OrgId)
require.Equal(t, tc.ExpectedEnabled, cmd.Enabled)
require.Equal(t, tc.ExpectedPluginVersion, cmd.PluginVersion)
}
})
}
type testConfigReader struct {
result []*pluginsAsConfig
err error
}
func (tcr *testConfigReader) readConfig(path string) ([]*pluginsAsConfig, error) {
return tcr.result, tcr.err
}
#sfxzgnsxzcvnbzcvn
cvbn
cvbn
c
vbn
cvbncvbn
\ No newline at end of file
apps:
- type: $ENABLE_PLUGIN_VAR
org_id: 2
disabled: false
- type: test-plugin-2
org_id: 3
disabled: true
- type: test-plugin
org_name: Org 3
- type: test-plugin-2
# Ignore everything in this directory
*
# Except this file
!.gitignore
\ No newline at end of file
package plugins
import "github.com/grafana/grafana/pkg/services/provisioning/values"
// pluginsAsConfig is a normalized data object for plugins config data. Any config version should be mappable.
// to this type.
type pluginsAsConfig struct {
Apps []*appFromConfig
}
type appFromConfig struct {
OrgID int64
OrgName string
PluginID string
Enabled bool
Pinned bool
PluginVersion string
JSONData map[string]interface{}
SecureJSONData map[string]string
}
type appFromConfigV0 struct {
OrgID values.Int64Value `json:"org_id" yaml:"org_id"`
OrgName values.StringValue `json:"org_name" yaml:"org_name"`
Type values.StringValue `json:"type" yaml:"type"`
Disabled values.BoolValue `json:"disabled" yaml:"disabled"`
JSONData values.JSONValue `json:"jsonData" yaml:"jsonData"`
SecureJSONData values.StringMapValue `json:"secureJsonData" yaml:"secureJsonData"`
}
// pluginsAsConfigV0 is a mapping for zero version configs. This is mapped to its normalised version.
type pluginsAsConfigV0 struct {
Apps []*appFromConfigV0 `json:"apps" yaml:"apps"`
}
// mapToPluginsFromConfig maps config syntax to a normalized notificationsAsConfig object. Every version
// of the config syntax should have this function.
func (cfg *pluginsAsConfigV0) mapToPluginsFromConfig() *pluginsAsConfig {
r := &pluginsAsConfig{}
if cfg == nil {
return r
}
for _, app := range cfg.Apps {
r.Apps = append(r.Apps, &appFromConfig{
OrgID: app.OrgID.Value(),
OrgName: app.OrgName.Value(),
PluginID: app.Type.Value(),
Enabled: !app.Disabled.Value(),
Pinned: true,
JSONData: app.JSONData.Value(),
SecureJSONData: app.SecureJSONData.Value(),
})
}
return r
}
...@@ -6,17 +6,18 @@ import ( ...@@ -6,17 +6,18 @@ import (
"sync" "sync"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/dashboards"
"github.com/grafana/grafana/pkg/services/provisioning/datasources" "github.com/grafana/grafana/pkg/services/provisioning/datasources"
"github.com/grafana/grafana/pkg/services/provisioning/notifiers" "github.com/grafana/grafana/pkg/services/provisioning/notifiers"
"github.com/grafana/grafana/pkg/services/provisioning/plugins"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
) )
type ProvisioningService interface { type ProvisioningService interface {
ProvisionDatasources() error ProvisionDatasources() error
ProvisionPlugins() error
ProvisionNotifications() error ProvisionNotifications() error
ProvisionDashboards() error ProvisionDashboards() error
GetDashboardProvisionerResolvedPath(name string) string GetDashboardProvisionerResolvedPath(name string) string
...@@ -30,6 +31,7 @@ func init() { ...@@ -30,6 +31,7 @@ func init() {
}, },
notifiers.Provision, notifiers.Provision,
datasources.Provision, datasources.Provision,
plugins.Provision,
)) ))
} }
...@@ -37,12 +39,14 @@ func NewProvisioningServiceImpl( ...@@ -37,12 +39,14 @@ func NewProvisioningServiceImpl(
newDashboardProvisioner dashboards.DashboardProvisionerFactory, newDashboardProvisioner dashboards.DashboardProvisionerFactory,
provisionNotifiers func(string) error, provisionNotifiers func(string) error,
provisionDatasources func(string) error, provisionDatasources func(string) error,
provisionPlugins func(string) error,
) *provisioningServiceImpl { ) *provisioningServiceImpl {
return &provisioningServiceImpl{ return &provisioningServiceImpl{
log: log.New("provisioning"), log: log.New("provisioning"),
newDashboardProvisioner: newDashboardProvisioner, newDashboardProvisioner: newDashboardProvisioner,
provisionNotifiers: provisionNotifiers, provisionNotifiers: provisionNotifiers,
provisionDatasources: provisionDatasources, provisionDatasources: provisionDatasources,
provisionPlugins: provisionPlugins,
} }
} }
...@@ -54,6 +58,7 @@ type provisioningServiceImpl struct { ...@@ -54,6 +58,7 @@ type provisioningServiceImpl struct {
dashboardProvisioner dashboards.DashboardProvisioner dashboardProvisioner dashboards.DashboardProvisioner
provisionNotifiers func(string) error provisionNotifiers func(string) error
provisionDatasources func(string) error provisionDatasources func(string) error
provisionPlugins func(string) error
mutex sync.Mutex mutex sync.Mutex
} }
...@@ -63,6 +68,11 @@ func (ps *provisioningServiceImpl) Init() error { ...@@ -63,6 +68,11 @@ func (ps *provisioningServiceImpl) Init() error {
return err return err
} }
err = ps.ProvisionPlugins()
if err != nil {
return err
}
err = ps.ProvisionNotifications() err = ps.ProvisionNotifications()
if err != nil { if err != nil {
return err return err
...@@ -107,6 +117,12 @@ func (ps *provisioningServiceImpl) ProvisionDatasources() error { ...@@ -107,6 +117,12 @@ func (ps *provisioningServiceImpl) ProvisionDatasources() error {
return errutil.Wrap("Datasource provisioning error", err) return errutil.Wrap("Datasource provisioning error", err)
} }
func (ps *provisioningServiceImpl) ProvisionPlugins() error {
appPath := path.Join(ps.Cfg.ProvisioningPath, "plugins")
err := ps.provisionPlugins(appPath)
return errutil.Wrap("app provisioning error", err)
}
func (ps *provisioningServiceImpl) ProvisionNotifications() error { func (ps *provisioningServiceImpl) ProvisionNotifications() error {
alertNotificationsPath := path.Join(ps.Cfg.ProvisioningPath, "notifiers") alertNotificationsPath := path.Join(ps.Cfg.ProvisioningPath, "notifiers")
err := ps.provisionNotifiers(alertNotificationsPath) err := ps.provisionNotifiers(alertNotificationsPath)
......
...@@ -2,6 +2,7 @@ package provisioning ...@@ -2,6 +2,7 @@ package provisioning
type Calls struct { type Calls struct {
ProvisionDatasources []interface{} ProvisionDatasources []interface{}
ProvisionPlugins []interface{}
ProvisionNotifications []interface{} ProvisionNotifications []interface{}
ProvisionDashboards []interface{} ProvisionDashboards []interface{}
GetDashboardProvisionerResolvedPath []interface{} GetDashboardProvisionerResolvedPath []interface{}
...@@ -11,6 +12,7 @@ type Calls struct { ...@@ -11,6 +12,7 @@ type Calls struct {
type ProvisioningServiceMock struct { type ProvisioningServiceMock struct {
Calls *Calls Calls *Calls
ProvisionDatasourcesFunc func() error ProvisionDatasourcesFunc func() error
ProvisionPluginsFunc func() error
ProvisionNotificationsFunc func() error ProvisionNotificationsFunc func() error
ProvisionDashboardsFunc func() error ProvisionDashboardsFunc func() error
GetDashboardProvisionerResolvedPathFunc func(name string) string GetDashboardProvisionerResolvedPathFunc func(name string) string
...@@ -31,6 +33,14 @@ func (mock *ProvisioningServiceMock) ProvisionDatasources() error { ...@@ -31,6 +33,14 @@ func (mock *ProvisioningServiceMock) ProvisionDatasources() error {
return nil return nil
} }
func (mock *ProvisioningServiceMock) ProvisionPlugins() error {
mock.Calls.ProvisionPlugins = append(mock.Calls.ProvisionPlugins, nil)
if mock.ProvisionPluginsFunc != nil {
return mock.ProvisionPluginsFunc()
}
return nil
}
func (mock *ProvisioningServiceMock) ProvisionNotifications() error { func (mock *ProvisioningServiceMock) ProvisionNotifications() error {
mock.Calls.ProvisionNotifications = append(mock.Calls.ProvisionNotifications, nil) mock.Calls.ProvisionNotifications = append(mock.Calls.ProvisionNotifications, nil)
if mock.ProvisionNotificationsFunc != nil { if mock.ProvisionNotificationsFunc != nil {
......
...@@ -97,6 +97,7 @@ func setup() *serviceTestStruct { ...@@ -97,6 +97,7 @@ func setup() *serviceTestStruct {
}, },
nil, nil,
nil, nil,
nil,
) )
serviceTest.service.Cfg = setting.NewCfg() serviceTest.service.Cfg = setting.NewCfg()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment