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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user