Provisioning: Add API endpoint to reload provisioning configs (#16579)

* Add api to reaload provisioning

* Refactor and simplify the polling code

* Add test for the provisioning service

* Fix provider initialization and move some code to file reader

* Simplify the code and move initialization

* Remove unused code

* Update comment

* Add comment

* Change error messages

* Add DashboardProvisionerFactory type

* Update imports

* Use new assert lib

* Use mutext for synchronizing the reloading

* Fix typo

Co-Authored-By: aocenas <mr.ocenas@gmail.com>

* Add docs about the new api
This commit is contained in:
Andrej Ocenas
2019-04-25 09:06:44 +02:00
committed by GitHub
parent b3bfbc6f77
commit 42b745a098
9 changed files with 382 additions and 73 deletions
@@ -3,44 +3,79 @@ package dashboards
import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/log"
"github.com/pkg/errors"
)
type DashboardProvisioner struct {
cfgReader *configReader
log log.Logger
type DashboardProvisioner interface {
Provision() error
PollChanges(ctx context.Context)
}
func NewDashboardProvisioner(configDirectory string) *DashboardProvisioner {
log := log.New("provisioning.dashboard")
d := &DashboardProvisioner{
cfgReader: &configReader{path: configDirectory, log: log},
log: log,
}
return d
type DashboardProvisionerImpl struct {
log log.Logger
fileReaders []*fileReader
}
func (provider *DashboardProvisioner) Provision(ctx context.Context) error {
cfgs, err := provider.cfgReader.readConfig()
type DashboardProvisionerFactory func(string) (DashboardProvisioner, error)
func NewDashboardProvisionerImpl(configDirectory string) (*DashboardProvisionerImpl, error) {
logger := log.New("provisioning.dashboard")
cfgReader := &configReader{path: configDirectory, log: logger}
configs, err := cfgReader.readConfig()
if err != nil {
return err
return nil, errors.Wrap(err, "Failed to read dashboards config")
}
for _, cfg := range cfgs {
switch cfg.Type {
case "file":
fileReader, err := NewDashboardFileReader(cfg, provider.log.New("type", cfg.Type, "name", cfg.Name))
if err != nil {
return err
}
fileReaders, err := getFileReaders(configs, logger)
go fileReader.ReadAndListen(ctx)
default:
return fmt.Errorf("type %s is not supported", cfg.Type)
if err != nil {
return nil, errors.Wrap(err, "Failed to initialize file readers")
}
d := &DashboardProvisionerImpl{
log: logger,
fileReaders: fileReaders,
}
return d, nil
}
func (provider *DashboardProvisionerImpl) Provision() error {
for _, reader := range provider.fileReaders {
err := reader.startWalkingDisk()
if err != nil {
return errors.Wrapf(err, "Failed to provision config %v", reader.Cfg.Name)
}
}
return nil
}
// PollChanges starts polling for changes in dashboard definition files. It creates goroutine for each provider
// defined in the config.
func (provider *DashboardProvisionerImpl) PollChanges(ctx context.Context) {
for _, reader := range provider.fileReaders {
go reader.pollChanges(ctx)
}
}
func getFileReaders(configs []*DashboardsAsConfig, logger log.Logger) ([]*fileReader, error) {
var readers []*fileReader
for _, config := range configs {
switch config.Type {
case "file":
fileReader, err := NewDashboardFileReader(config, logger.New("type", config.Type, "name", config.Name))
if err != nil {
return nil, errors.Wrapf(err, "Failed to create file reader for config %v", config.Name)
}
readers = append(readers, fileReader)
default:
return nil, fmt.Errorf("type %s is not supported", config.Type)
}
}
return readers, nil
}
@@ -0,0 +1,36 @@
package dashboards
import "context"
type Calls struct {
Provision []interface{}
PollChanges []interface{}
}
type DashboardProvisionerMock struct {
Calls *Calls
ProvisionFunc func() error
PollChangesFunc func(ctx context.Context)
}
func NewDashboardProvisionerMock() *DashboardProvisionerMock {
return &DashboardProvisionerMock{
Calls: &Calls{},
}
}
func (dpm *DashboardProvisionerMock) Provision() error {
dpm.Calls.Provision = append(dpm.Calls.Provision, nil)
if dpm.ProvisionFunc != nil {
return dpm.ProvisionFunc()
} else {
return nil
}
}
func (dpm *DashboardProvisionerMock) PollChanges(ctx context.Context) {
dpm.Calls.PollChanges = append(dpm.Calls.PollChanges, ctx)
if dpm.PollChangesFunc != nil {
dpm.PollChangesFunc(ctx)
}
}
@@ -51,35 +51,25 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade
}, nil
}
func (fr *fileReader) ReadAndListen(ctx context.Context) error {
if err := fr.startWalkingDisk(); err != nil {
fr.log.Error("failed to search for dashboards", "error", err)
}
ticker := time.NewTicker(time.Duration(int64(time.Second) * fr.Cfg.UpdateIntervalSeconds))
running := false
// pollChanges periodically runs startWalkingDisk based on interval specified in the config.
func (fr *fileReader) pollChanges(ctx context.Context) {
ticker := time.Tick(time.Duration(int64(time.Second) * fr.Cfg.UpdateIntervalSeconds))
for {
select {
case <-ticker.C:
if !running { // avoid walking the filesystem in parallel. in-case fs is very slow.
running = true
go func() {
if err := fr.startWalkingDisk(); err != nil {
fr.log.Error("failed to search for dashboards", "error", err)
}
running = false
}()
case <-ticker:
if err := fr.startWalkingDisk(); err != nil {
fr.log.Error("failed to search for dashboards", "error", err)
}
case <-ctx.Done():
return nil
return
}
}
}
// startWalkingDisk finds and saves dashboards on disk.
// startWalkingDisk traverses the file system for defined path, reads dashboard definition files and applies any change
// to the database.
func (fr *fileReader) startWalkingDisk() error {
fr.log.Debug("Start walking disk", "path", fr.Path)
resolvedPath := fr.resolvePath(fr.Path)
if _, err := os.Stat(resolvedPath); err != nil {
if os.IsNotExist(err) {