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 {
Result *ValidateDashboardBeforeSaveResult
}
type DeleteOrphanedProvisionedDashboardsCommand struct {
ReaderNames []string
}
//
// QUERIES
//
......
......@@ -5,7 +5,9 @@ import (
"fmt"
"os"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util/errutil"
)
......@@ -16,6 +18,7 @@ type DashboardProvisioner interface {
PollChanges(ctx context.Context)
GetProvisionerResolvedPath(name string) string
GetAllowUIUpdatesFromConfig(name string) bool
CleanUpOrphanedDashboards()
}
// DashboardProvisionerFactory creates DashboardProvisioners based on input
......@@ -71,6 +74,19 @@ func (provider *Provisioner) Provision() error {
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
// defined in the config.
func (provider *Provisioner) PollChanges(ctx context.Context) {
......
......@@ -60,3 +60,6 @@ func (dpm *ProvisionerMock) GetAllowUIUpdatesFromConfig(name string) bool {
}
return false
}
// CleanUpOrphanedDashboards not implemented for mocks
func (dpm *ProvisionerMock) CleanUpOrphanedDashboards() {}
......@@ -143,9 +143,11 @@ func (ps *provisioningServiceImpl) ProvisionDashboards() error {
defer ps.mutex.Unlock()
ps.cancelPolling()
dashProvisioner.CleanUpOrphanedDashboards()
if err := dashProvisioner.Provision(); err != nil {
// If we fail to provision with the new provisioner, mutex will unlock and the polling we restart with the
err = dashProvisioner.Provision()
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 errutil.Wrap("Failed to provision dashboards", err)
}
......
......@@ -353,57 +353,61 @@ func GetDashboardTags(query *models.GetDashboardTagsQuery) error {
func DeleteDashboard(cmd *models.DeleteDashboardCommand) error {
return inTransaction(func(sess *DBSession) error {
dashboard := models.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId}
has, err := sess.Get(&dashboard)
return deleteDashboard(cmd, sess)
})
}
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 {
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 {
for _, id := range dashIds {
if err := deleteAlertDefinition(id.Id, sess); err != nil {
return err
}
for _, id := range dashIds {
if err := deleteAlertDefinition(id.Id, sess); err != nil {
return err
}
}
}
}
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
return err
}
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
return err
}
for _, sql := range deletes {
_, err := sess.Exec(sql, dashboard.Id)
for _, sql := range deletes {
_, err := sess.Exec(sql, dashboard.Id)
if err != nil {
return err
}
if err != nil {
return err
}
}
return nil
})
return nil
}
func GetDashboards(query *models.GetDashboardsQuery) error {
......
package sqlstore
import (
"errors"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
)
......@@ -10,6 +12,7 @@ func init() {
bus.AddHandler("sql", SaveProvisionedDashboard)
bus.AddHandler("sql", GetProvisionedDataByDashboardId)
bus.AddHandler("sql", UnprovisionDashboard)
bus.AddHandler("sql", DeleteOrphanedProvisionedDashboards)
}
type DashboardExtras struct {
......@@ -88,3 +91,26 @@ func UnprovisionDashboard(cmd *models.UnprovisionDashboardCommand) error {
}
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) {
So(cmd.Result.Id, ShouldNotEqual, 0)
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() {
query := &models.GetProvisionedDashboardDataQuery{Name: "default"}
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