mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Provisioning: Remove provisioned dashboards without parental reader (#26143)
This commit is contained in:
@@ -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 {
|
||||
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
_, err := sess.Exec(sql, dashboard.Id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
_, err := sess.Exec(sql, dashboard.Id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDashboards(query *models.GetDashboardsQuery) error {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user