Files
grafana/pkg/services/provisioning/provisioning.go
Will Browne 05b458741e DataSources: Add mutex to provisioning step (#106921)
add mutex to ds provisioning
2025-06-18 16:34:40 +01:00

353 lines
14 KiB
Go

package provisioning
import (
"context"
"errors"
"fmt"
"path/filepath"
"sync"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/correlations"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards"
datasourceservice "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/folder"
alertingauthz "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
alertstore "github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
prov_alerting "github.com/grafana/grafana/pkg/services/provisioning/alerting"
"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
"github.com/grafana/grafana/pkg/services/provisioning/plugins"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
)
func ProvideService(
ac accesscontrol.AccessControl,
cfg *setting.Cfg,
sqlStore db.DB,
pluginStore pluginstore.Store,
alertingStore *alertstore.DBstore,
encryptionService encryption.Internal,
notificatonService *notifications.NotificationService,
dashboardProvisioningService dashboardservice.DashboardProvisioningService,
datasourceService datasourceservice.DataSourceService,
correlationsService correlations.Service,
dashboardService dashboardservice.DashboardService,
folderService folder.Service,
pluginSettings pluginsettings.Service,
searchService searchV2.SearchService,
quotaService quota.Service,
secrectService secrets.Service,
orgService org.Service,
resourcePermissions accesscontrol.ReceiverPermissionsService,
tracer tracing.Tracer,
dual dualwrite.Service,
) (*ProvisioningServiceImpl, error) {
s := &ProvisioningServiceImpl{
Cfg: cfg,
SQLStore: sqlStore,
ac: ac,
pluginStore: pluginStore,
alertingStore: alertingStore,
EncryptionService: encryptionService,
NotificationService: notificatonService,
newDashboardProvisioner: dashboards.New,
provisionDatasources: datasources.Provision,
provisionPlugins: plugins.Provision,
provisionAlerting: prov_alerting.Provision,
dashboardProvisioningService: dashboardProvisioningService,
dashboardService: dashboardService,
datasourceService: datasourceService,
correlationsService: correlationsService,
pluginsSettings: pluginSettings,
searchService: searchService,
quotaService: quotaService,
secretService: secrectService,
log: log.New("provisioning"),
orgService: orgService,
folderService: folderService,
resourcePermissions: resourcePermissions,
tracer: tracer,
}
if err := s.setDashboardProvisioner(); err != nil {
return nil, err
}
return s, nil
}
func (ps *ProvisioningServiceImpl) setDashboardProvisioner() error {
dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards")
dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.orgService, ps.dashboardService, ps.folderService, ps.dual)
if err != nil {
return fmt.Errorf("%v: %w", "Failed to create provisioner", err)
}
ps.dashboardProvisioner = dashProvisioner
return nil
}
type ProvisioningService interface {
registry.BackgroundService
RunInitProvisioners(ctx context.Context) error
ProvisionDatasources(ctx context.Context) error
ProvisionPlugins(ctx context.Context) error
ProvisionDashboards(ctx context.Context) error
ProvisionAlerting(ctx context.Context) error
GetDashboardProvisionerResolvedPath(name string) string
GetAllowUIUpdatesFromConfig(name string) bool
}
// Used for testing purposes
func newProvisioningServiceImpl(
newDashboardProvisioner dashboards.DashboardProvisionerFactory,
provisionDatasources func(context.Context, string, datasources.BaseDataSourceService, datasources.CorrelationsStore, org.Service) error,
provisionPlugins func(context.Context, string, pluginstore.Store, pluginsettings.Service, org.Service) error,
searchService searchV2.SearchService,
) (*ProvisioningServiceImpl, error) {
s := &ProvisioningServiceImpl{
log: log.New("provisioning"),
newDashboardProvisioner: newDashboardProvisioner,
provisionDatasources: provisionDatasources,
provisionPlugins: provisionPlugins,
Cfg: setting.NewCfg(),
searchService: searchService,
}
if err := s.setDashboardProvisioner(); err != nil {
return nil, err
}
return s, nil
}
type ProvisioningServiceImpl struct {
Cfg *setting.Cfg
SQLStore db.DB
orgService org.Service
ac accesscontrol.AccessControl
pluginStore pluginstore.Store
alertingStore *alertstore.DBstore
EncryptionService encryption.Internal
NotificationService *notifications.NotificationService
log log.Logger
pollingCtxCancel context.CancelFunc
newDashboardProvisioner dashboards.DashboardProvisionerFactory
dashboardProvisioner dashboards.DashboardProvisioner
provisionDatasources func(context.Context, string, datasources.BaseDataSourceService, datasources.CorrelationsStore, org.Service) error
provisionPlugins func(context.Context, string, pluginstore.Store, pluginsettings.Service, org.Service) error
provisionAlerting func(context.Context, prov_alerting.ProvisionerConfig) error
mutex sync.Mutex
dashboardProvisioningService dashboardservice.DashboardProvisioningService
dashboardService dashboardservice.DashboardService
datasourceService datasourceservice.DataSourceService
correlationsService correlations.Service
pluginsSettings pluginsettings.Service
searchService searchV2.SearchService
quotaService quota.Service
secretService secrets.Service
folderService folder.Service
resourcePermissions accesscontrol.ReceiverPermissionsService
tracer tracing.Tracer
dual dualwrite.Service
onceInitProvisioners sync.Once
}
func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error {
// We had to move the initialization of OSS provisioners to Run()
// because they need the /apis/* endpoints to be ready and listening.
// They query these endpoints to retrieve folders and dashboards.
return nil
}
func (ps *ProvisioningServiceImpl) Run(ctx context.Context) error {
var err error
// Run Datasources, Plugins and Alerting Provisioning only once.
// It can't be initialized at RunInitProvisioners because it
// depends on the /apis endpoints to be already running and listeningq
ps.onceInitProvisioners.Do(func() {
err = ps.ProvisionDatasources(ctx)
if err != nil {
ps.log.Error("Failed to provision data sources", "error", err)
return
}
err = ps.ProvisionPlugins(ctx)
if err != nil {
ps.log.Error("Failed to provision plugins", "error", err)
return
}
err = ps.ProvisionAlerting(ctx)
if err != nil {
ps.log.Error("Failed to provision alerting", "error", err)
return
}
})
if err != nil {
// error already logged
return err
}
err = ps.ProvisionDashboards(ctx)
if err != nil {
ps.log.Error("Failed to provision dashboard", "error", err)
// Consider the allow list of errors for which running the provisioning service should not
// fail. For now this includes only dashboards.ErrGetOrCreateFolder.
if !errors.Is(err, dashboards.ErrGetOrCreateFolder) {
return err
}
}
if ps.dashboardProvisioner.HasDashboardSources() {
ps.searchService.TriggerReIndex()
}
for {
// Wait for unlock. This is tied to new dashboardProvisioner to be instantiated before we start polling.
ps.mutex.Lock()
// Using background here because otherwise if root context was canceled the select later on would
// non-deterministically take one of the route possibly going into one polling loop before exiting.
pollingContext, cancelFun := context.WithCancel(context.Background())
ps.pollingCtxCancel = cancelFun
ps.dashboardProvisioner.PollChanges(pollingContext)
ps.mutex.Unlock()
select {
case <-pollingContext.Done():
// Polling was canceled.
continue
case <-ctx.Done():
// Root server context was cancelled so cancel polling and leave.
ps.cancelPolling()
return ctx.Err()
}
}
}
func (ps *ProvisioningServiceImpl) ProvisionDatasources(ctx context.Context) error {
ps.mutex.Lock()
defer ps.mutex.Unlock()
datasourcePath := filepath.Join(ps.Cfg.ProvisioningPath, "datasources")
if err := ps.provisionDatasources(ctx, datasourcePath, ps.datasourceService, ps.correlationsService, ps.orgService); err != nil {
err = fmt.Errorf("%v: %w", "Datasource provisioning error", err)
ps.log.Error("Failed to provision data sources", "error", err)
return err
}
return nil
}
func (ps *ProvisioningServiceImpl) ProvisionPlugins(ctx context.Context) error {
ps.mutex.Lock()
defer ps.mutex.Unlock()
appPath := filepath.Join(ps.Cfg.ProvisioningPath, "plugins")
if err := ps.provisionPlugins(ctx, appPath, ps.pluginStore, ps.pluginsSettings, ps.orgService); err != nil {
err = fmt.Errorf("%v: %w", "app provisioning error", err)
ps.log.Error("Failed to provision plugins", "error", err)
return err
}
return nil
}
func (ps *ProvisioningServiceImpl) ProvisionDashboards(ctx context.Context) error {
err := ps.setDashboardProvisioner()
if err != nil {
return fmt.Errorf("%v: %w", "Failed to create provisioner", err)
}
ps.mutex.Lock()
defer ps.mutex.Unlock()
ps.cancelPolling()
ps.dashboardProvisioner.CleanUpOrphanedDashboards(ctx)
err = ps.dashboardProvisioner.Provision(ctx)
if err != nil {
// If we fail to provision with the new provisioner, the mutex will unlock and the polling will restart with the
// old provisioner as we did not switch them yet.
return fmt.Errorf("%v: %w", "Failed to provision dashboards", err)
}
return nil
}
func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error {
alertingPath := filepath.Join(ps.Cfg.ProvisioningPath, "alerting")
ruleService := provisioning.NewAlertRuleService(
ps.alertingStore,
ps.alertingStore,
ps.folderService,
//ps.dashboardService,
ps.quotaService,
ps.SQLStore,
int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
int64(ps.Cfg.UnifiedAlerting.BaseInterval.Seconds()),
ps.Cfg.UnifiedAlerting.RulesPerRuleGroupLimit,
ps.log,
notifier.NewCachedNotificationSettingsValidationService(ps.alertingStore),
alertingauthz.NewRuleService(ps.ac),
)
configStore := legacy_storage.NewAlertmanagerConfigStore(ps.alertingStore)
receiverSvc := notifier.NewReceiverService(
alertingauthz.NewReceiverAccess[*ngmodels.Receiver](ps.ac, true),
configStore,
ps.alertingStore,
ps.alertingStore,
ps.secretService,
ps.SQLStore,
ps.log,
ps.resourcePermissions,
ps.tracer,
)
contactPointService := provisioning.NewContactPointService(configStore, ps.secretService,
ps.alertingStore, ps.SQLStore, receiverSvc, ps.log, ps.alertingStore, ps.resourcePermissions)
notificationPolicyService := provisioning.NewNotificationPolicyService(configStore,
ps.alertingStore, ps.SQLStore, ps.Cfg.UnifiedAlerting, ps.log)
mutetimingsService := provisioning.NewMuteTimingService(configStore, ps.alertingStore, ps.alertingStore, ps.log, ps.alertingStore)
templateService := provisioning.NewTemplateService(configStore, ps.alertingStore, ps.alertingStore, ps.log)
cfg := prov_alerting.ProvisionerConfig{
Path: alertingPath,
RuleService: *ruleService,
FolderService: ps.folderService,
DashboardProvService: ps.dashboardProvisioningService,
ContactPointService: *contactPointService,
NotificiationPolicyService: *notificationPolicyService,
MuteTimingService: *mutetimingsService,
TemplateService: *templateService,
}
return ps.provisionAlerting(ctx, cfg)
}
func (ps *ProvisioningServiceImpl) GetDashboardProvisionerResolvedPath(name string) string {
return ps.dashboardProvisioner.GetProvisionerResolvedPath(name)
}
func (ps *ProvisioningServiceImpl) GetAllowUIUpdatesFromConfig(name string) bool {
return ps.dashboardProvisioner.GetAllowUIUpdatesFromConfig(name)
}
func (ps *ProvisioningServiceImpl) cancelPolling() {
if ps.pollingCtxCancel != nil {
ps.log.Debug("Stop polling for dashboard changes")
ps.pollingCtxCancel()
}
ps.pollingCtxCancel = nil
}