Files
grafana/pkg/services/dashboards/service/provisioning_cleanup_test.go
Renato Costa c73cab8eef chore: add cleanup task for duplicated provisioned dashboards (#115103)
* chore: add cleanup task for duplicated provisioned dashboards
2025-12-12 13:56:47 -05:00

280 lines
10 KiB
Go

package service
import (
"testing"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/stretchr/testify/require"
)
func Test_canBeAutomaticallyCleanedUp(t *testing.T) {
testCases := []struct {
name string
configs []dashboards.ProvisioningConfig
expectedSkip string
}{
{
name: "no dashboards defined in the configuration",
configs: []dashboards.ProvisioningConfig{},
expectedSkip: "no provisioned dashboards",
},
{
name: "first defined dashboard has no folder defined",
configs: []dashboards.ProvisioningConfig{
{Name: "1", Folder: ""},
{Folder: "f1"},
},
expectedSkip: "dashboard has no folder: 1",
},
{
name: "one of the provisioned dashboards has no folder defined",
configs: []dashboards.ProvisioningConfig{
{Name: "1", Folder: "f1"},
{Name: "2", Folder: "f1"},
{Name: "3", Folder: ""},
{Name: "4", Folder: "f1"},
},
expectedSkip: "dashboards provisioned across multiple folders",
},
{
name: "one of the provisioned dashboards allows UI updates",
configs: []dashboards.ProvisioningConfig{
{Name: "1", Folder: "f1"},
{Name: "2", Folder: "f1", AllowUIUpdates: true},
{Name: "3", Folder: "f1"},
{Name: "4", Folder: "f1"},
},
expectedSkip: "contains dashboards with allowUiUpdates",
},
{
name: "one of the provisioned dashboards is in a different folder",
configs: []dashboards.ProvisioningConfig{
{Name: "1", Folder: "f1"},
{Name: "2", Folder: "f1"},
{Name: "3", Folder: "f1"},
{Name: "4", Folder: "different"},
},
expectedSkip: "dashboards provisioned across multiple folders",
},
{
name: "can be skipped when all conditions are met",
configs: []dashboards.ProvisioningConfig{
{Name: "1", Folder: "f1"},
{Name: "2", Folder: "f1"},
{Name: "3", Folder: "f1"},
{Name: "4", Folder: "f1"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.expectedSkip, canBeAutomaticallyCleanedUp(tc.configs))
})
}
}
func Test_cleanupSteps(t *testing.T) {
isDashboard, isFolder := false, true
fromUser := func(uid, name string, isFolder bool) dashboards.DashboardSearchProjection {
return dashboards.DashboardSearchProjection{
UID: uid,
ManagerId: name,
IsFolder: isFolder,
}
}
provisioned := func(uid, name string, isFolder bool) dashboards.DashboardSearchProjection {
dashboard := fromUser(uid, name, isFolder)
dashboard.ManagedBy = utils.ManagerKindClassicFP //nolint:staticcheck
return dashboard
}
testCases := []struct {
name string
provisionedFolders []string
provisionedResources []dashboards.DashboardSearchProjection
configDashboards []string
expectedSteps []deleteProvisionedResource
expectedErr string
}{
{
name: "no provisioned folders, nothing to do",
provisionedFolders: []string{},
configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3"},
provisionedResources: []dashboards.DashboardSearchProjection{
provisioned("d1", "Provisioned1", isDashboard),
},
},
{
name: "multiple folders, a user-created dashboard in one of them",
provisionedFolders: []string{"folder1", "folder2"},
configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3"},
provisionedResources: []dashboards.DashboardSearchProjection{
provisioned("d1", "Provisioned1", isDashboard),
provisioned("d2", "Provisioned2", isDashboard),
fromUser("d3", "User1", isDashboard),
provisioned("d4", "Provisioned3", isDashboard),
},
expectedErr: "multiple provisioning folders exist with at least one user-created resource",
},
{
name: "multiple folders, a user-created folder in one of them",
provisionedFolders: []string{"folder1", "folder2"},
configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3", "Provisioned4"},
provisionedResources: []dashboards.DashboardSearchProjection{
provisioned("d1", "Provisioned1", isDashboard),
provisioned("d2", "Provisioned2", isDashboard),
provisioned("d3", "Provisioned3", isDashboard),
fromUser("f1", "UserFolder1", isFolder),
},
expectedErr: "multiple provisioning folders exist with at least one user-created resource",
},
{
name: "single folder, some dashboards duplicated",
provisionedFolders: []string{"folder1"},
configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3", "Provisioned4"},
provisionedResources: []dashboards.DashboardSearchProjection{
// Provisioned1 is duplicated.
provisioned("d1", "Provisioned1", isDashboard),
provisioned("d2", "Provisioned2", isDashboard),
provisioned("d3", "Provisioned1", isDashboard),
provisioned("d4", "Provisioned3", isDashboard),
},
expectedSteps: []deleteProvisionedResource{
{Type: searchstore.TypeDashboard, UID: "d1"},
{Type: searchstore.TypeDashboard, UID: "d2"},
{Type: searchstore.TypeDashboard, UID: "d3"},
{Type: searchstore.TypeDashboard, UID: "d4"},
},
},
{
name: "single folder, duplicated dashboards, user-created dashboards are ignored",
provisionedFolders: []string{"folder1"},
configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3", "Provisioned4"},
provisionedResources: []dashboards.DashboardSearchProjection{
// Provisioned1 is duplicated.
provisioned("d1", "Provisioned1", isDashboard),
provisioned("d2", "Provisioned2", isDashboard),
fromUser("d3", "User1", isDashboard),
provisioned("d4", "Provisioned3", isDashboard),
provisioned("d5", "Provisioned1", isDashboard),
},
// User dashboard (d3) is not deleted.
expectedSteps: []deleteProvisionedResource{
{Type: searchstore.TypeDashboard, UID: "d1"},
{Type: searchstore.TypeDashboard, UID: "d2"},
{Type: searchstore.TypeDashboard, UID: "d4"},
{Type: searchstore.TypeDashboard, UID: "d5"},
},
},
{
name: "single folder, duplicated dashboards, user-created folders are ignored",
provisionedFolders: []string{"folder1"},
configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3"},
provisionedResources: []dashboards.DashboardSearchProjection{
// Provisioned1 is duplicated.
provisioned("d1", "Provisioned1", isDashboard),
provisioned("d2", "Provisioned2", isDashboard),
provisioned("d3", "Provisioned3", isDashboard),
provisioned("d4", "Provisioned1", isDashboard),
fromUser("f1", "UserFolder1", isFolder),
},
// User folder (f1) is not deleted.
expectedSteps: []deleteProvisionedResource{
{Type: searchstore.TypeDashboard, UID: "d1"},
{Type: searchstore.TypeDashboard, UID: "d2"},
{Type: searchstore.TypeDashboard, UID: "d3"},
{Type: searchstore.TypeDashboard, UID: "d4"},
},
},
{
name: "multiple folders, only provisioned dashboards",
provisionedFolders: []string{"folder1", "folder2"},
configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3", "Provisioned4"},
provisionedResources: []dashboards.DashboardSearchProjection{
provisioned("d1", "Provisioned1", isDashboard),
provisioned("d2", "Provisioned2", isDashboard),
provisioned("d3", "Provisioned3", isDashboard),
provisioned("d4", "Provisioned4", isDashboard),
},
// Delete all dashboards, then all folders.
expectedSteps: []deleteProvisionedResource{
{Type: searchstore.TypeDashboard, UID: "d1"},
{Type: searchstore.TypeDashboard, UID: "d2"},
{Type: searchstore.TypeDashboard, UID: "d3"},
{Type: searchstore.TypeDashboard, UID: "d4"},
{Type: searchstore.TypeFolder, UID: "folder1"},
{Type: searchstore.TypeFolder, UID: "folder2"},
},
},
{
name: "single folder, only deletes dashboards defined in the config file",
provisionedFolders: []string{"folder1"},
configDashboards: []string{"Provisioned1", "Provisioned2"},
provisionedResources: []dashboards.DashboardSearchProjection{
provisioned("d1", "Provisioned1", isDashboard),
provisioned("d2", "Provisioned2", isDashboard),
provisioned("d3", "Provisioned1", isDashboard),
provisioned("d4", "Provisioned4", isDashboard),
provisioned("d5", "Provisioned4", isDashboard),
},
// Delete duplicated dashboards, but keep Provisioned4, since it's not in the config file.
expectedSteps: []deleteProvisionedResource{
{Type: searchstore.TypeDashboard, UID: "d1"},
{Type: searchstore.TypeDashboard, UID: "d2"},
{Type: searchstore.TypeDashboard, UID: "d3"},
},
},
{
name: "single folder, no duplicated dashboards",
provisionedFolders: []string{"folder1"},
configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3", "Provisioned4"},
provisionedResources: []dashboards.DashboardSearchProjection{
provisioned("d1", "Provisioned1", isDashboard),
provisioned("d2", "Provisioned2", isDashboard),
provisioned("d3", "Provisioned3", isDashboard),
provisioned("d4", "Provisioned4", isDashboard),
},
expectedSteps: nil, // no duplicates, nothing to do
},
{
name: "single folder, no duplicated dashboards, multiple user-created resources",
provisionedFolders: []string{"folder1"},
configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3", "Provisioned4"},
provisionedResources: []dashboards.DashboardSearchProjection{
provisioned("d1", "Provisioned1", isDashboard),
provisioned("d2", "Provisioned2", isDashboard),
fromUser("f1", "UserFolder1", isFolder),
provisioned("d3", "Provisioned3", isDashboard),
fromUser("d4", "User1", isDashboard),
provisioned("d5", "Provisioned4", isDashboard),
fromUser("d6", "User2", isDashboard),
fromUser("f2", "UserFolder2", isFolder),
},
expectedSteps: nil, // no duplicates in the provisioned set, nothing to do
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
provisionedSet := make(map[string]bool)
for _, name := range tc.configDashboards {
provisionedSet[name] = true
}
steps, err := cleanupSteps(tc.provisionedFolders, tc.provisionedResources, provisionedSet)
if tc.expectedErr == "" {
require.NoError(t, err)
require.Equal(t, tc.expectedSteps, steps)
} else {
require.Error(t, err)
require.Equal(t, tc.expectedErr, err.Error())
}
})
}
}