diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 42996fd9ead..b133cf231df 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -384,6 +384,10 @@ type ValidateDashboardBeforeSaveCommand struct { Result *ValidateDashboardBeforeSaveResult } +type DeleteOrphanedProvisionedDashboardsCommand struct { + ReaderNames []string +} + // // QUERIES // diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index 5bd14ead792..dfd5298980b 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -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) { diff --git a/pkg/services/provisioning/dashboards/dashboard_mock.go b/pkg/services/provisioning/dashboards/dashboard_mock.go index 63107b5f8e5..d2cd191fb1c 100644 --- a/pkg/services/provisioning/dashboards/dashboard_mock.go +++ b/pkg/services/provisioning/dashboards/dashboard_mock.go @@ -60,3 +60,6 @@ func (dpm *ProvisionerMock) GetAllowUIUpdatesFromConfig(name string) bool { } return false } + +// CleanUpOrphanedDashboards not implemented for mocks +func (dpm *ProvisionerMock) CleanUpOrphanedDashboards() {} diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 1f66bc496ce..94c108d16d7 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -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) } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 70ff51a1a56..99443348544 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -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 { diff --git a/pkg/services/sqlstore/dashboard_provisioning.go b/pkg/services/sqlstore/dashboard_provisioning.go index 48238477ce9..77f9b2b9498 100644 --- a/pkg/services/sqlstore/dashboard_provisioning.go +++ b/pkg/services/sqlstore/dashboard_provisioning.go @@ -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 +} diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go index b58829c92f8..9c8b5ead793 100644 --- a/pkg/services/sqlstore/dashboard_provisioning_test.go +++ b/pkg/services/sqlstore/dashboard_provisioning_test.go @@ -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)