Commit 6e3e0dea by Maksim Nabokikh Committed by GitHub

Provisioning: Remove provisioned dashboards without parental reader (#26143)

parent 0132bca9
...@@ -384,6 +384,10 @@ type ValidateDashboardBeforeSaveCommand struct { ...@@ -384,6 +384,10 @@ type ValidateDashboardBeforeSaveCommand struct {
Result *ValidateDashboardBeforeSaveResult Result *ValidateDashboardBeforeSaveResult
} }
type DeleteOrphanedProvisionedDashboardsCommand struct {
ReaderNames []string
}
// //
// QUERIES // QUERIES
// //
......
...@@ -5,7 +5,9 @@ import ( ...@@ -5,7 +5,9 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/util/errutil"
) )
...@@ -16,6 +18,7 @@ type DashboardProvisioner interface { ...@@ -16,6 +18,7 @@ type DashboardProvisioner interface {
PollChanges(ctx context.Context) PollChanges(ctx context.Context)
GetProvisionerResolvedPath(name string) string GetProvisionerResolvedPath(name string) string
GetAllowUIUpdatesFromConfig(name string) bool GetAllowUIUpdatesFromConfig(name string) bool
CleanUpOrphanedDashboards()
} }
// DashboardProvisionerFactory creates DashboardProvisioners based on input // DashboardProvisionerFactory creates DashboardProvisioners based on input
...@@ -71,6 +74,19 @@ func (provider *Provisioner) Provision() error { ...@@ -71,6 +74,19 @@ func (provider *Provisioner) Provision() error {
return nil return nil
} }
// CleanUpOrphanedDashboards deletes provisioned dashboards missing a linked reader.
func (provider *Provisioner) CleanUpOrphanedDashboards() {
currentReaders := make([]string, len(provider.fileReaders))
for index, reader := range provider.fileReaders {
currentReaders[index] = reader.Cfg.Name
}
if err := bus.Dispatch(&models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: currentReaders}); err != nil {
provider.log.Warn("Failed to delete orphaned provisioned dashboards", "err", err)
}
}
// PollChanges starts polling for changes in dashboard definition files. It creates goroutine for each provider // PollChanges starts polling for changes in dashboard definition files. It creates goroutine for each provider
// defined in the config. // defined in the config.
func (provider *Provisioner) PollChanges(ctx context.Context) { func (provider *Provisioner) PollChanges(ctx context.Context) {
......
...@@ -60,3 +60,6 @@ func (dpm *ProvisionerMock) GetAllowUIUpdatesFromConfig(name string) bool { ...@@ -60,3 +60,6 @@ func (dpm *ProvisionerMock) GetAllowUIUpdatesFromConfig(name string) bool {
} }
return false return false
} }
// CleanUpOrphanedDashboards not implemented for mocks
func (dpm *ProvisionerMock) CleanUpOrphanedDashboards() {}
...@@ -143,9 +143,11 @@ func (ps *provisioningServiceImpl) ProvisionDashboards() error { ...@@ -143,9 +143,11 @@ func (ps *provisioningServiceImpl) ProvisionDashboards() error {
defer ps.mutex.Unlock() defer ps.mutex.Unlock()
ps.cancelPolling() ps.cancelPolling()
dashProvisioner.CleanUpOrphanedDashboards()
if err := dashProvisioner.Provision(); err != nil { err = dashProvisioner.Provision()
// If we fail to provision with the new provisioner, mutex will unlock and the polling we restart with the 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. // old provisioner as we did not switch them yet.
return errutil.Wrap("Failed to provision dashboards", err) return errutil.Wrap("Failed to provision dashboards", err)
} }
......
...@@ -353,57 +353,61 @@ func GetDashboardTags(query *models.GetDashboardTagsQuery) error { ...@@ -353,57 +353,61 @@ func GetDashboardTags(query *models.GetDashboardTagsQuery) error {
func DeleteDashboard(cmd *models.DeleteDashboardCommand) error { func DeleteDashboard(cmd *models.DeleteDashboardCommand) error {
return inTransaction(func(sess *DBSession) error { return inTransaction(func(sess *DBSession) error {
dashboard := models.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId} return deleteDashboard(cmd, sess)
has, err := sess.Get(&dashboard) })
}
func deleteDashboard(cmd *models.DeleteDashboardCommand, sess *DBSession) error {
dashboard := models.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId}
has, err := sess.Get(&dashboard)
if err != nil {
return err
} else if !has {
return models.ErrDashboardNotFound
}
deletes := []string{
"DELETE FROM dashboard_tag WHERE dashboard_id = ? ",
"DELETE FROM star WHERE dashboard_id = ? ",
"DELETE FROM dashboard WHERE id = ?",
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
"DELETE FROM annotation WHERE dashboard_id = ?",
"DELETE FROM dashboard_provisioning WHERE dashboard_id = ?",
}
if dashboard.IsFolder {
deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)")
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
dashIds := []struct {
Id int64
}{}
err := sess.SQL("select id from dashboard where folder_id = ?", dashboard.Id).Find(&dashIds)
if err != nil { if err != nil {
return err return err
} else if !has {
return models.ErrDashboardNotFound
}
deletes := []string{
"DELETE FROM dashboard_tag WHERE dashboard_id = ? ",
"DELETE FROM star WHERE dashboard_id = ? ",
"DELETE FROM dashboard WHERE id = ?",
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
"DELETE FROM annotation WHERE dashboard_id = ?",
"DELETE FROM dashboard_provisioning WHERE dashboard_id = ?",
} }
if dashboard.IsFolder { for _, id := range dashIds {
deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)") if err := deleteAlertDefinition(id.Id, sess); err != nil {
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
dashIds := []struct {
Id int64
}{}
err := sess.SQL("select id from dashboard where folder_id = ?", dashboard.Id).Find(&dashIds)
if err != nil {
return err return err
} }
for _, id := range dashIds {
if err := deleteAlertDefinition(id.Id, sess); err != nil {
return err
}
}
} }
}
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil { if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
return err return err
} }
for _, sql := range deletes { for _, sql := range deletes {
_, err := sess.Exec(sql, dashboard.Id) _, err := sess.Exec(sql, dashboard.Id)
if err != nil { if err != nil {
return err return err
}
} }
}
return nil return nil
})
} }
func GetDashboards(query *models.GetDashboardsQuery) error { func GetDashboards(query *models.GetDashboardsQuery) error {
......
package sqlstore package sqlstore
import ( import (
"errors"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
) )
...@@ -10,6 +12,7 @@ func init() { ...@@ -10,6 +12,7 @@ func init() {
bus.AddHandler("sql", SaveProvisionedDashboard) bus.AddHandler("sql", SaveProvisionedDashboard)
bus.AddHandler("sql", GetProvisionedDataByDashboardId) bus.AddHandler("sql", GetProvisionedDataByDashboardId)
bus.AddHandler("sql", UnprovisionDashboard) bus.AddHandler("sql", UnprovisionDashboard)
bus.AddHandler("sql", DeleteOrphanedProvisionedDashboards)
} }
type DashboardExtras struct { type DashboardExtras struct {
...@@ -88,3 +91,26 @@ func UnprovisionDashboard(cmd *models.UnprovisionDashboardCommand) error { ...@@ -88,3 +91,26 @@ func UnprovisionDashboard(cmd *models.UnprovisionDashboardCommand) error {
} }
return nil return nil
} }
func DeleteOrphanedProvisionedDashboards(cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error {
var result []*models.DashboardProvisioning
convertedReaderNames := make([]interface{}, len(cmd.ReaderNames))
for index, readerName := range cmd.ReaderNames {
convertedReaderNames[index] = readerName
}
err := x.NotIn("name", convertedReaderNames...).Find(&result)
if err != nil {
return err
}
for _, deleteDashCommand := range result {
err := DeleteDashboard(&models.DeleteDashboardCommand{Id: deleteDashCommand.DashboardId})
if err != nil && !errors.Is(err, models.ErrDashboardNotFound) {
return err
}
}
return nil
}
...@@ -54,6 +54,43 @@ func TestDashboardProvisioningTest(t *testing.T) { ...@@ -54,6 +54,43 @@ func TestDashboardProvisioningTest(t *testing.T) {
So(cmd.Result.Id, ShouldNotEqual, 0) So(cmd.Result.Id, ShouldNotEqual, 0)
dashId := cmd.Result.Id dashId := cmd.Result.Id
Convey("Deleting orphaned provisioned dashboards", func() {
anotherCmd := &models.SaveProvisionedDashboardCommand{
DashboardCmd: &models.SaveDashboardCommand{
OrgId: 1,
IsFolder: false,
FolderId: folderCmd.Result.Id,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "another_dashboard",
}),
},
DashboardProvisioning: &models.DashboardProvisioning{
Name: "another_reader",
ExternalId: "/var/grafana.json",
Updated: now.Unix(),
},
}
err := SaveProvisionedDashboard(anotherCmd)
So(err, ShouldBeNil)
query := &models.GetDashboardsQuery{DashboardIds: []int64{anotherCmd.Result.Id}}
err = GetDashboards(query)
So(err, ShouldBeNil)
So(query.Result, ShouldNotBeNil)
deleteCmd := &models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}}
So(DeleteOrphanedProvisionedDashboards(deleteCmd), ShouldBeNil)
query = &models.GetDashboardsQuery{DashboardIds: []int64{cmd.Result.Id, anotherCmd.Result.Id}}
err = GetDashboards(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Id, ShouldEqual, dashId)
})
Convey("Can query for provisioned dashboards", func() { Convey("Can query for provisioned dashboards", func() {
query := &models.GetProvisionedDashboardDataQuery{Name: "default"} query := &models.GetProvisionedDashboardDataQuery{Name: "default"}
err := GetProvisionedDashboardDataQuery(query) err := GetProvisionedDashboardDataQuery(query)
......
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