|
|
|
@@ -6,13 +6,17 @@ import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"slices"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
|
|
|
|
"github.com/prometheus/common/model"
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
@@ -1988,3 +1992,137 @@ func TestIntegrationDeleteNestedFoldersPostorder(t *testing.T) {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test deleting folder with provisioned dashboard has proper handling with forceDeleteRules
|
|
|
|
|
func TestIntegrationDeleteFolderWithProvisionedDashboards(t *testing.T) {
|
|
|
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
|
|
|
|
|
|
if !db.IsTestDbSQLite() {
|
|
|
|
|
t.Skip("test only on sqlite for now")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for mode := 0; mode <= 5; mode++ {
|
|
|
|
|
t.Run(fmt.Sprintf("Mode %d: Delete provisioned folders and dashboards", mode), func(t *testing.T) {
|
|
|
|
|
modeDw := grafanarest.DualWriterMode(mode)
|
|
|
|
|
ops := testinfra.GrafanaOpts{
|
|
|
|
|
DisableAnonymous: true,
|
|
|
|
|
AppModeProduction: true,
|
|
|
|
|
APIServerStorageType: "unified",
|
|
|
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
|
|
|
folders.RESOURCEGROUP: {
|
|
|
|
|
DualWriterMode: modeDw,
|
|
|
|
|
},
|
|
|
|
|
"dashboards.dashboard.grafana.app": {
|
|
|
|
|
DualWriterMode: modeDw,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
EnableFeatureToggles: []string{
|
|
|
|
|
featuremgmt.FlagUnifiedStorageSearch,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
// Setup Grafana with provisioning
|
|
|
|
|
ops.Dir, ops.DirPath = testinfra.CreateGrafDir(t, ops)
|
|
|
|
|
// Create provisioning directories
|
|
|
|
|
provDashboardsDir := fmt.Sprintf("%s/conf/provisioning/dashboards", ops.Dir)
|
|
|
|
|
provDashboardsCfg := fmt.Sprintf("%s/dev.yaml", provDashboardsDir)
|
|
|
|
|
blob := []byte(fmt.Sprintf(`
|
|
|
|
|
apiVersion: 1
|
|
|
|
|
|
|
|
|
|
providers:
|
|
|
|
|
- name: 'provisioned dashboards'
|
|
|
|
|
type: file
|
|
|
|
|
orgId: 1
|
|
|
|
|
folder: 'GrafanaCloud'
|
|
|
|
|
options:
|
|
|
|
|
path: %s`, provDashboardsDir))
|
|
|
|
|
err := os.WriteFile(provDashboardsCfg, blob, 0o644)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
input, err := os.ReadFile(filepath.Join("testdata/dashboard.json"))
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
provDashboardFile := filepath.Join(provDashboardsDir, "dashboard.json")
|
|
|
|
|
err = os.WriteFile(provDashboardFile, input, 0o644)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
helper := apis.NewK8sTestHelper(t, ops)
|
|
|
|
|
|
|
|
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
|
|
|
User: helper.Org1.Admin,
|
|
|
|
|
GVR: gvr,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
var folderUID string
|
|
|
|
|
var dashboardUID string
|
|
|
|
|
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
|
|
|
|
resp := apis.DoRequest(helper, apis.RequestParams{
|
|
|
|
|
User: client.Args.User,
|
|
|
|
|
Method: http.MethodGet,
|
|
|
|
|
Path: fmt.Sprintf("/apis/dashboard.grafana.app/v0alpha1/namespaces/%s/search?query=dashboard&limit=50&type=dashboard", client.Args.Namespace),
|
|
|
|
|
}, &map[string]interface{}{})
|
|
|
|
|
var list v0alpha1.SearchResults
|
|
|
|
|
require.NotNil(t, resp.Response)
|
|
|
|
|
assert.Equal(t, http.StatusOK, resp.Response.StatusCode)
|
|
|
|
|
assert.NoError(t, json.Unmarshal(resp.Body, &list))
|
|
|
|
|
assert.Equal(collect, list.TotalHits, int64(1), "Dashboard should be ready")
|
|
|
|
|
for _, d := range list.Hits {
|
|
|
|
|
folderUID = d.Folder
|
|
|
|
|
dashboardUID = d.Name
|
|
|
|
|
}
|
|
|
|
|
}, 10*time.Second, 25*time.Millisecond)
|
|
|
|
|
|
|
|
|
|
_, err = client.Resource.Get(context.Background(), folderUID, metav1.GetOptions{})
|
|
|
|
|
require.NoError(t, err, "folder %s should exist", folderUID)
|
|
|
|
|
// Verify dashboards exist
|
|
|
|
|
verifyDashboardExists := func(shouldExist bool) {
|
|
|
|
|
getDash := apis.DoRequest(helper, apis.RequestParams{
|
|
|
|
|
User: client.Args.User,
|
|
|
|
|
Method: http.MethodGet,
|
|
|
|
|
Path: fmt.Sprintf("/apis/dashboard.grafana.app/v1beta1/namespaces/default/dashboards/%s", dashboardUID),
|
|
|
|
|
}, &map[string]interface{}{})
|
|
|
|
|
if shouldExist {
|
|
|
|
|
require.Equal(t, http.StatusOK, getDash.Response.StatusCode, "dashboard %s should exist", dashboardUID)
|
|
|
|
|
} else {
|
|
|
|
|
require.Equal(t, http.StatusNotFound, getDash.Response.StatusCode, "dashboard %s should not exist", dashboardUID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
verifyDashboardExists(true)
|
|
|
|
|
|
|
|
|
|
t.Run("Deletion should fail when forceDeleteRules=false with provisioned dashboards", func(t *testing.T) {
|
|
|
|
|
// Attempt to delete the parent folder without forceDeleteRules
|
|
|
|
|
parentDeleteNoForce := apis.DoRequest(helper, apis.RequestParams{
|
|
|
|
|
User: client.Args.User,
|
|
|
|
|
Method: http.MethodDelete,
|
|
|
|
|
Path: fmt.Sprintf("/api/folders/%s?forceDeleteRules=false", folderUID),
|
|
|
|
|
}, &folder.Folder{})
|
|
|
|
|
|
|
|
|
|
// Should fail because provisioned dashboards cannot be deleted without forceDeleteRules
|
|
|
|
|
require.Equal(t, http.StatusBadRequest, parentDeleteNoForce.Response.StatusCode, "deletion should fail without forceDeleteRules")
|
|
|
|
|
require.Contains(t, string(parentDeleteNoForce.Body), dashboards.ErrDashboardCannotDeleteProvisionedDashboard.Reason, "error message should indicate provisioned dashboards cannot be deleted")
|
|
|
|
|
// Verify folders still exist
|
|
|
|
|
_, err := client.Resource.Get(context.Background(), folderUID, metav1.GetOptions{})
|
|
|
|
|
require.NoError(t, err, "parent folder %d should still exist", folderUID)
|
|
|
|
|
// Verify dashboard still exist
|
|
|
|
|
verifyDashboardExists(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("Deletion should succeed when forceDeleteRules=true and delete provisioned dashboards", func(t *testing.T) {
|
|
|
|
|
_, err = client.Resource.Get(context.Background(), folderUID, metav1.GetOptions{})
|
|
|
|
|
require.NoError(t, err, "parent folder %d should still exist", folderUID)
|
|
|
|
|
// Delete the parent folder with forceDeleteRules=true
|
|
|
|
|
parentDelete := apis.DoRequest(helper, apis.RequestParams{
|
|
|
|
|
User: client.Args.User,
|
|
|
|
|
Method: http.MethodDelete,
|
|
|
|
|
Path: fmt.Sprintf("/api/folders/%s?forceDeleteRules=true", folderUID),
|
|
|
|
|
}, &folder.Folder{})
|
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusOK, parentDelete.Response.StatusCode, "deletion should succeed with forceDeleteRules")
|
|
|
|
|
|
|
|
|
|
// Verify folders was deleted
|
|
|
|
|
_, err := client.Resource.Get(context.Background(), folderUID, metav1.GetOptions{})
|
|
|
|
|
require.Error(t, err, "parent folder %s should not exist", folderUID)
|
|
|
|
|
// Verify provisioned dashboard is deleted
|
|
|
|
|
verifyDashboardExists(false)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|