2186 lines
74 KiB
Go
2186 lines
74 KiB
Go
package folder
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"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"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
|
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
|
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
|
"github.com/grafana/grafana/pkg/expr"
|
|
"github.com/grafana/grafana/pkg/infra/db"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
alerting "github.com/grafana/grafana/pkg/tests/api/alerting"
|
|
"github.com/grafana/grafana/pkg/tests/apis"
|
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
|
"github.com/grafana/grafana/pkg/tests/testsuite"
|
|
"github.com/grafana/grafana/pkg/util/testutil"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
testsuite.Run(m)
|
|
}
|
|
|
|
var gvr = schema.GroupVersionResource{
|
|
Group: folders.GROUP,
|
|
Version: folders.VERSION,
|
|
Resource: "folders",
|
|
}
|
|
|
|
func TestIntegrationFoldersApp(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
if !db.IsTestDbSQLite() {
|
|
t.Skip("test only on sqlite for now")
|
|
}
|
|
|
|
t.Run("Check discovery client", func(t *testing.T) {
|
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
EnableFeatureToggles: []string{},
|
|
})
|
|
disco := helper.NewDiscoveryClient()
|
|
resources, err := disco.ServerResourcesForGroupVersion("folder.grafana.app/v1beta1")
|
|
require.NoError(t, err)
|
|
|
|
v1Disco, err := json.MarshalIndent(resources, "", " ")
|
|
require.NoError(t, err)
|
|
|
|
require.JSONEq(t, `{
|
|
"kind": "APIResourceList",
|
|
"apiVersion": "v1",
|
|
"groupVersion": "folder.grafana.app/v1beta1",
|
|
"resources": [
|
|
{
|
|
"name": "folders",
|
|
"singularName": "folder",
|
|
"namespaced": true,
|
|
"kind": "Folder",
|
|
"verbs": [
|
|
"create",
|
|
"delete",
|
|
"deletecollection",
|
|
"get",
|
|
"list",
|
|
"patch",
|
|
"update"
|
|
]
|
|
},
|
|
{
|
|
"name": "folders/access",
|
|
"singularName": "",
|
|
"namespaced": true,
|
|
"kind": "FolderAccessInfo",
|
|
"verbs": [
|
|
"get"
|
|
]
|
|
},
|
|
{
|
|
"name": "folders/children",
|
|
"singularName": "",
|
|
"namespaced": true,
|
|
"kind": "FolderList",
|
|
"verbs": [
|
|
"get"
|
|
]
|
|
},
|
|
{
|
|
"name": "folders/counts",
|
|
"singularName": "",
|
|
"namespaced": true,
|
|
"kind": "DescendantCounts",
|
|
"verbs": [
|
|
"get"
|
|
]
|
|
},
|
|
{
|
|
"name": "folders/parents",
|
|
"singularName": "",
|
|
"namespaced": true,
|
|
"kind": "FolderInfoList",
|
|
"verbs": [
|
|
"get"
|
|
]
|
|
}
|
|
]
|
|
}`, string(v1Disco))
|
|
})
|
|
|
|
// test on all dualwriter modes
|
|
for mode := 0; mode <= 4; mode++ {
|
|
modeDw := grafanarest.DualWriterMode(mode)
|
|
|
|
t.Run(fmt.Sprintf("with dual write (unified storage, mode %v)", modeDw), func(t *testing.T) {
|
|
doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
EnableFeatureToggles: []string{},
|
|
}))
|
|
})
|
|
|
|
t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create nested folders)", modeDw), func(t *testing.T) {
|
|
doNestedCreateTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
}))
|
|
})
|
|
|
|
t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create existing folder)", modeDw), func(t *testing.T) {
|
|
doCreateDuplicateFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
}))
|
|
})
|
|
|
|
t.Run(fmt.Sprintf("when creating a folder, mode %v, it should trim leading and trailing spaces", modeDw), func(t *testing.T) {
|
|
doCreateEnsureTitleIsTrimmedTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
}))
|
|
})
|
|
|
|
t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create circular reference folder)", modeDw), func(t *testing.T) {
|
|
doCreateCircularReferenceFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
}))
|
|
})
|
|
}
|
|
|
|
// This is a general test for the unified storage list operation. We don't have a common test
|
|
// directory for now, so we (search and storage) keep it here as we own this part of the tests.
|
|
t.Run("make sure list works with continue tokens", func(t *testing.T) {
|
|
t.Skip("Skipping flaky test - list works with continue tokens")
|
|
modes := []grafanarest.DualWriterMode{
|
|
grafanarest.Mode1,
|
|
grafanarest.Mode2,
|
|
grafanarest.Mode3,
|
|
grafanarest.Mode4,
|
|
grafanarest.Mode5,
|
|
}
|
|
for _, mode := range modes {
|
|
t.Run(fmt.Sprintf("mode %d", mode), func(t *testing.T) {
|
|
doListFoldersTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: mode,
|
|
},
|
|
},
|
|
// We set it to 1 here, so we always get forced pagination based on the response size.
|
|
UnifiedStorageMaxPageSizeBytes: 1,
|
|
}), mode)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// Validates that folder delete checks alert_rule stats and blocks deletion
|
|
func TestIntegrationFolderDeletionBlockedByAlertRules(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
if !db.IsTestDbSQLite() {
|
|
t.Skip("test only on sqlite for now")
|
|
}
|
|
|
|
t.Run("should be blocked by alert rules", func(t *testing.T) {
|
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {DualWriterMode: grafanarest.Mode5},
|
|
},
|
|
UnifiedStorageEnableSearch: true,
|
|
})
|
|
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
// Create a folder via legacy API so it is visible everywhere.
|
|
folderUID := "alertrule-del-test"
|
|
legacyPayload := fmt.Sprintf(`{"title": "Folder With Alert Rule", "uid": "%s"}`, folderUID)
|
|
legacyCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(legacyPayload),
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, legacyCreate.Result)
|
|
require.Equal(t, folderUID, legacyCreate.Result.UID)
|
|
|
|
// Create one alert rule in that folder namespace via ruler API.
|
|
addr := helper.GetEnv().Server.HTTPServer.Listener.Addr().String()
|
|
api := alerting.NewAlertingLegacyAPIClient(addr, "admin", "admin")
|
|
|
|
// simple always-true rule
|
|
forDuration := model.Duration(10 * time.Second)
|
|
rule := apimodels.PostableExtendedRuleNode{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{For: &forDuration},
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "rule-in-folder",
|
|
Condition: "A",
|
|
Data: []apimodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
DatasourceUID: expr.DatasourceUID,
|
|
RelativeTimeRange: apimodels.RelativeTimeRange{
|
|
From: apimodels.Duration(600 * time.Second),
|
|
To: 0,
|
|
},
|
|
Model: json.RawMessage(`{"type":"math","expression":"2 + 3 > 1"}`),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
group := apimodels.PostableRuleGroupConfig{
|
|
Name: "arulegroup",
|
|
Interval: model.Duration(10 * time.Second),
|
|
Rules: []apimodels.PostableExtendedRuleNode{rule},
|
|
}
|
|
_ = api.PostRulesGroup(t, folderUID, &group, false)
|
|
|
|
// Attempt to delete the folder via K8s API. This should be blocked by alert rules.
|
|
err := client.Resource.Delete(context.Background(), folderUID, metav1.DeleteOptions{})
|
|
require.Error(t, err, "expected folder deletion to be blocked when alert rules exist")
|
|
|
|
// Delete the rule group from ruler.
|
|
status, body := api.DeleteRulesGroup(t, folderUID, group.Name, true)
|
|
require.Equalf(t, http.StatusAccepted, status, body)
|
|
|
|
// Now we should be able to delete the folder.
|
|
err = client.Resource.Delete(context.Background(), folderUID, metav1.DeleteOptions{})
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func doFolderTests(t *testing.T, helper *apis.K8sTestHelper) *apis.K8sTestHelper {
|
|
t.Run("Check folder CRUD (just create for now) in legacy API appears in k8s apis", func(t *testing.T) {
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
// #TODO: figure out permissions topic
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
// #TODO fill out the payload: parentUID, description
|
|
// and check about uid orgid and siU
|
|
legacyPayload := `{
|
|
"title": "Test",
|
|
"uid": ""
|
|
}`
|
|
legacyCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(legacyPayload),
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, legacyCreate.Result)
|
|
uid := legacyCreate.Result.UID
|
|
require.NotEmpty(t, uid)
|
|
//nolint:staticcheck
|
|
id := legacyCreate.Result.ID
|
|
require.NotEmpty(t, id)
|
|
idStr := fmt.Sprintf("%d", id)
|
|
|
|
expectedResult := `{
|
|
"apiVersion": "folder.grafana.app/v1beta1",
|
|
"kind": "Folder",
|
|
"metadata": {
|
|
"creationTimestamp": "${creationTimestamp}",
|
|
"labels": {"grafana.app/deprecatedInternalID":"` + idStr + `"},
|
|
"name": "` + uid + `",
|
|
"namespace": "default",
|
|
"resourceVersion": "${resourceVersion}",
|
|
"uid": "${uid}"
|
|
},
|
|
"spec": {
|
|
"title": "Test",
|
|
"description": ""
|
|
}}`
|
|
|
|
// Get should return the same result
|
|
found, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.JSONEq(t, expectedResult, client.SanitizeJSON(found))
|
|
})
|
|
|
|
t.Run("Do CRUD (just CR+List for now) via k8s (and check that legacy api still works)", func(t *testing.T) {
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
// #TODO: figure out permissions topic
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
// Create the folder "test"
|
|
first, err := client.Resource.Create(context.Background(),
|
|
helper.LoadYAMLOrJSONFile("testdata/folder-test-create.yaml"),
|
|
metav1.CreateOptions{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "test", first.GetName())
|
|
uids := []string{first.GetName()}
|
|
|
|
// Create (with name generation) two folders
|
|
for i := 0; i < 2; i++ {
|
|
out, err := client.Resource.Create(context.Background(),
|
|
helper.LoadYAMLOrJSONFile("testdata/folder-generate.yaml"),
|
|
metav1.CreateOptions{},
|
|
)
|
|
require.NoError(t, err)
|
|
uids = append(uids, out.GetName())
|
|
}
|
|
slices.Sort(uids) // make list compare stable
|
|
|
|
// Check all folders
|
|
for _, uid := range uids {
|
|
getFromBothAPIs(t, helper, client, uid, nil)
|
|
}
|
|
|
|
// PUT :: Update the title
|
|
updated, err := client.Resource.Update(context.Background(),
|
|
helper.LoadYAMLOrJSONFile("testdata/folder-test-replace.yaml"),
|
|
metav1.UpdateOptions{},
|
|
)
|
|
require.NoError(t, err)
|
|
spec, ok := updated.Object["spec"].(map[string]any)
|
|
require.True(t, ok)
|
|
title, ok := spec["title"].(string)
|
|
require.True(t, ok)
|
|
description, ok := spec["description"].(string)
|
|
require.True(t, ok)
|
|
require.Equal(t, first.GetName(), updated.GetName())
|
|
require.Equal(t, first.GetUID(), updated.GetUID())
|
|
require.Equal(t, "Test folder (replaced from k8s; 1 item; PUT)", title)
|
|
require.Equal(t, "New description", description)
|
|
|
|
// #TODO figure out why this breaks just for MySQL integration tests
|
|
// require.Less(t, first.GetResourceVersion(), updated.GetResourceVersion())
|
|
|
|
// ensure that we get 4 items when listing via k8s
|
|
l, err := client.Resource.List(context.Background(), metav1.ListOptions{})
|
|
require.NoError(t, err)
|
|
folders, err := meta.ExtractList(l)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, folders)
|
|
require.Equal(t, len(folders), 4)
|
|
|
|
// delete test
|
|
errDelete := client.Resource.Delete(context.Background(), first.GetName(), metav1.DeleteOptions{})
|
|
require.NoError(t, errDelete)
|
|
})
|
|
return helper
|
|
}
|
|
|
|
// This does a get with both k8s and legacy API, and verifies the results are the same
|
|
func getFromBothAPIs(t *testing.T,
|
|
helper *apis.K8sTestHelper,
|
|
client *apis.K8sResourceClient,
|
|
uid string,
|
|
// Optionally match some expect some values
|
|
expect *folder.Folder,
|
|
) *unstructured.Unstructured {
|
|
t.Helper()
|
|
|
|
found, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, uid, found.GetName())
|
|
|
|
dto := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodGet,
|
|
Path: "/api/folders/" + uid,
|
|
}, &folder.Folder{}).Result
|
|
require.NotNil(t, dto)
|
|
require.Equal(t, uid, dto.UID)
|
|
|
|
spec, ok := found.Object["spec"].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, dto.UID, found.GetName())
|
|
require.Equal(t, dto.Title, spec["title"])
|
|
// #TODO add checks for other fields
|
|
|
|
if expect != nil {
|
|
if expect.Title != "" {
|
|
require.Equal(t, expect.Title, dto.Title)
|
|
require.Equal(t, expect.Title, spec["title"])
|
|
}
|
|
if expect.UID != "" {
|
|
require.Equal(t, expect.UID, dto.UID)
|
|
require.Equal(t, expect.UID, found.GetName())
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
func doNestedCreateTest(t *testing.T, helper *apis.K8sTestHelper) {
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
parentPayload := `{
|
|
"title": "Test/parent",
|
|
"uid": ""
|
|
}`
|
|
parentCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(parentPayload),
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, parentCreate.Result)
|
|
// creating a folder without providing a parent should default to the empty parent folder
|
|
require.Empty(t, parentCreate.Result.ParentUID)
|
|
|
|
parentUID := parentCreate.Result.UID
|
|
require.NotEmpty(t, parentUID)
|
|
|
|
childPayload := fmt.Sprintf(`{
|
|
"title": "Test/child",
|
|
"uid": "",
|
|
"parentUid": "%s"
|
|
}`, parentUID)
|
|
childCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(childPayload),
|
|
}, &dtos.Folder{})
|
|
require.NotNil(t, childCreate.Result)
|
|
childUID := childCreate.Result.UID
|
|
require.NotEmpty(t, childUID)
|
|
require.Equal(t, "Test/child", childCreate.Result.Title)
|
|
require.Equal(t, 1, len(childCreate.Result.Parents))
|
|
|
|
parent := childCreate.Result.Parents[0]
|
|
// creating a folder with a known parent should succeed
|
|
require.Equal(t, parentUID, childCreate.Result.ParentUID)
|
|
require.Equal(t, parentUID, parent.UID)
|
|
require.Equal(t, "Test/parent", parent.Title)
|
|
require.Equal(t, parentCreate.Result.URL, parent.URL)
|
|
}
|
|
|
|
func doCreateDuplicateFolderTest(t *testing.T, helper *apis.K8sTestHelper) {
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
payload := `{
|
|
"title": "Test",
|
|
"uid": ""
|
|
}`
|
|
create := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(payload),
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, create.Result)
|
|
parentUID := create.Result.UID
|
|
require.NotEmpty(t, parentUID)
|
|
|
|
create2 := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(payload),
|
|
}, &folder.Folder{})
|
|
require.NotEmpty(t, create2.Response)
|
|
require.Equal(t, 200, create2.Response.StatusCode) // it is OK
|
|
}
|
|
|
|
func doCreateEnsureTitleIsTrimmedTest(t *testing.T, helper *apis.K8sTestHelper) {
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
payload := `{
|
|
"title": " my folder ",
|
|
"uid": ""
|
|
}`
|
|
|
|
// When creating a folder it should trim leading and trailing spaces in both dashboard and folder tables
|
|
create := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(payload),
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, create.Result)
|
|
require.Equal(t, "my folder", create.Result.Title)
|
|
}
|
|
|
|
func doCreateCircularReferenceFolderTest(t *testing.T, helper *apis.K8sTestHelper) {
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
payload := `{
|
|
"title": "Test",
|
|
"uid": "newFolder",
|
|
"parentUid: "newFolder",
|
|
}`
|
|
create := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(payload),
|
|
}, &folder.Folder{})
|
|
require.NotEmpty(t, create.Response)
|
|
require.Equal(t, 400, create.Response.StatusCode)
|
|
}
|
|
|
|
func doListFoldersTest(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest.DualWriterMode) {
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
foldersCount := 3
|
|
for i := 0; i < foldersCount; i++ {
|
|
payload, err := json.Marshal(map[string]interface{}{
|
|
"title": fmt.Sprintf("Test-%d", i),
|
|
"uid": fmt.Sprintf("uid-%d", i),
|
|
})
|
|
require.NoError(t, err)
|
|
parentCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: payload,
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, parentCreate.Result)
|
|
require.Equal(t, http.StatusOK, parentCreate.Response.StatusCode)
|
|
}
|
|
fetchedFolders, fetchItemsPerCall := checkListRequest(t, 1, client)
|
|
require.Equal(t, []string{"uid-0", "uid-1", "uid-2"}, fetchedFolders)
|
|
require.Equal(t, []int{1, 1, 1}, fetchItemsPerCall[:3])
|
|
|
|
// Now let's see if the iterator also works when we are limited by the page size, which should be set
|
|
// to 1 byte for this test. We only need to check that if we test unified storage as the primary storage,
|
|
// as legacy doesn't have such a page size limit.
|
|
if mode == grafanarest.Mode3 || mode == grafanarest.Mode4 || mode == grafanarest.Mode5 {
|
|
t.Run("check page size iterator", func(t *testing.T) {
|
|
fetchedFolders, fetchItemsPerCall := checkListRequest(t, 3, client)
|
|
require.Equal(t, []string{"uid-0", "uid-1", "uid-2"}, fetchedFolders)
|
|
require.Equal(t, []int{1, 1, 1}, fetchItemsPerCall[:3])
|
|
})
|
|
}
|
|
}
|
|
|
|
func checkListRequest(t *testing.T, limit int64, client *apis.K8sResourceClient) ([]string, []int) {
|
|
fetchedFolders := make([]string, 0, 3)
|
|
fetchItemsPerCall := make([]int, 0, 3)
|
|
continueToken := ""
|
|
for {
|
|
res, err := client.Resource.List(context.Background(), metav1.ListOptions{
|
|
Limit: limit,
|
|
Continue: continueToken,
|
|
})
|
|
require.NoError(t, err)
|
|
fetchItemsPerCall = append(fetchItemsPerCall, len(res.Items))
|
|
for _, item := range res.Items {
|
|
fetchedFolders = append(fetchedFolders, item.GetName())
|
|
}
|
|
continueToken = res.GetContinue()
|
|
if continueToken == "" {
|
|
break
|
|
}
|
|
}
|
|
return fetchedFolders, fetchItemsPerCall
|
|
}
|
|
|
|
func TestIntegrationFolderCreatePermissions(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
if !db.IsTestDbSQLite() {
|
|
t.Skip("test only on sqlite for now")
|
|
}
|
|
|
|
folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}"
|
|
folderWithParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\", \"parentUid\": \"parentuid\"}"
|
|
|
|
type testCase struct {
|
|
description string
|
|
input string
|
|
permissions []resourcepermissions.SetResourcePermissionCommand
|
|
expectedCode int
|
|
}
|
|
tcs := []testCase{
|
|
{
|
|
description: "creation of folder without parent succeeds given the correct request for creating a folder",
|
|
input: folderWithoutParentInput,
|
|
expectedCode: http.StatusOK,
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{
|
|
Actions: []string{"folders:create"},
|
|
Resource: "folders",
|
|
ResourceAttribute: "uid",
|
|
ResourceID: "*",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
description: "Should not be able to create a folder under the root with subfolder creation permissions",
|
|
input: folderWithoutParentInput,
|
|
expectedCode: http.StatusForbidden,
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{
|
|
Actions: []string{"folders:create"},
|
|
Resource: "folders",
|
|
ResourceAttribute: "uid",
|
|
ResourceID: "subfolder_uid",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
description: "Should not be able to create new folder under another folder without the right permissions",
|
|
input: folderWithParentInput,
|
|
expectedCode: http.StatusForbidden,
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{
|
|
Actions: []string{"folders:create"},
|
|
Resource: "folders",
|
|
ResourceAttribute: "uid",
|
|
ResourceID: "wrong_uid",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
description: "creation of folder without parent fails without permissions to create a folder",
|
|
input: folderWithoutParentInput,
|
|
expectedCode: http.StatusForbidden,
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{},
|
|
},
|
|
{
|
|
description: "creation of folder with parent succeeds given the correct request for creating a folder",
|
|
input: folderWithParentInput,
|
|
expectedCode: http.StatusOK,
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{
|
|
Actions: []string{"folders:create"},
|
|
Resource: "folders",
|
|
ResourceAttribute: "uid",
|
|
ResourceID: "parentuid",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// test on all dualwriter modes
|
|
for mode := 0; mode <= 4; mode++ {
|
|
t.Run(fmt.Sprintf("Mode_%d", mode), func(t *testing.T) {
|
|
modeDw := grafanarest.DualWriterMode(mode)
|
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
})
|
|
for i, tc := range tcs {
|
|
t.Run(fmt.Sprintf("[Mode: %v] "+tc.description, mode), func(t *testing.T) {
|
|
username := fmt.Sprintf("user-%d", i)
|
|
parentUID := fmt.Sprintf("parentuid-%d", i)
|
|
childUID := fmt.Sprintf("uid-%d", i)
|
|
|
|
// Update permissions to use unique parent UID
|
|
permissions := make([]resourcepermissions.SetResourcePermissionCommand, len(tc.permissions))
|
|
for j, perm := range tc.permissions {
|
|
permissions[j] = perm
|
|
if perm.ResourceID == "parentuid" {
|
|
permissions[j].ResourceID = parentUID
|
|
}
|
|
}
|
|
|
|
user := helper.CreateUser(username, apis.Org1, org.RoleViewer, permissions)
|
|
|
|
// Get user ID for cleanup
|
|
userID, _ := user.Identity.GetInternalID()
|
|
|
|
// Register cleanup for this test case
|
|
t.Cleanup(helper.CleanupTestResources([]string{parentUID, childUID}, []int64{userID}))
|
|
|
|
parentPayload := fmt.Sprintf(`{
|
|
"title": "Test/parent",
|
|
"uid": "%s"
|
|
}`, parentUID)
|
|
parentCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: helper.Org1.Admin,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(parentPayload),
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, parentCreate.Result)
|
|
createdParentUID := parentCreate.Result.UID
|
|
require.NotEmpty(t, createdParentUID)
|
|
|
|
// Update input to use unique UIDs
|
|
input := strings.ReplaceAll(tc.input, "parentuid", parentUID)
|
|
input = strings.ReplaceAll(input, `"uid": "uid"`, fmt.Sprintf(`"uid": "%s"`, childUID))
|
|
|
|
resp := apis.DoRequest(helper, apis.RequestParams{
|
|
User: user,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(input),
|
|
}, &dtos.Folder{})
|
|
require.Equal(t, tc.expectedCode, resp.Response.StatusCode)
|
|
|
|
if tc.expectedCode == http.StatusOK {
|
|
require.Equal(t, childUID, resp.Result.UID)
|
|
require.Equal(t, "Folder", resp.Result.Title)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIntegrationFolderGetPermissions(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
if !db.IsTestDbSQLite() {
|
|
t.Skip("test only on sqlite for now")
|
|
}
|
|
|
|
type testCase struct {
|
|
description string
|
|
permissions []resourcepermissions.SetResourcePermissionCommand
|
|
expectedCode int
|
|
expectedParentUIDs []string
|
|
expectedParentTitles []string
|
|
checkAccessControl bool
|
|
}
|
|
tcs := []testCase{
|
|
{
|
|
description: "get folder by UID should return parent folders if nested folder are enabled",
|
|
expectedCode: http.StatusOK,
|
|
expectedParentUIDs: []string{"parentuid"},
|
|
expectedParentTitles: []string{"testparent"},
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{
|
|
Actions: []string{dashboards.ActionFoldersRead},
|
|
Resource: "folders",
|
|
ResourceAttribute: "uid",
|
|
ResourceID: "*",
|
|
},
|
|
},
|
|
checkAccessControl: true,
|
|
},
|
|
{
|
|
description: "get folder by UID should not return parent folders if nested folder are enabled and user does not have read access to parent folders",
|
|
expectedCode: http.StatusOK,
|
|
expectedParentUIDs: []string{},
|
|
expectedParentTitles: []string{},
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{
|
|
Actions: []string{dashboards.ActionFoldersRead},
|
|
Resource: "folders",
|
|
ResourceAttribute: "uid",
|
|
ResourceID: "descuid",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
description: "get folder by UID should not succeed if user doesn't have permissions for the folder",
|
|
expectedCode: http.StatusForbidden,
|
|
expectedParentUIDs: []string{},
|
|
expectedParentTitles: []string{},
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{},
|
|
},
|
|
}
|
|
|
|
// test on all dualwriter modes
|
|
for mode := 0; mode <= 4; mode++ {
|
|
t.Run(fmt.Sprintf("Mode_%d", mode), func(t *testing.T) {
|
|
modeDw := grafanarest.DualWriterMode(mode)
|
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
})
|
|
|
|
// Run all test cases within the same server instance
|
|
for i, tc := range tcs {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
// Use unique UIDs per test case to avoid conflicts
|
|
parentUID := fmt.Sprintf("parentuid-%d", i)
|
|
descUID := fmt.Sprintf("descuid-%d", i)
|
|
parentTitle := fmt.Sprintf("testparent-%d", i)
|
|
userLogin := fmt.Sprintf("user-%d", i)
|
|
acUserLogin := fmt.Sprintf("acuser-%d", i)
|
|
|
|
// Create parent folder
|
|
parentPayload := fmt.Sprintf(`{
|
|
"title": "%s",
|
|
"uid": "%s"
|
|
}`, parentTitle, parentUID)
|
|
parentCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: helper.Org1.Admin,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(parentPayload),
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, parentCreate.Result)
|
|
require.Equal(t, parentUID, parentCreate.Result.UID)
|
|
|
|
// Create descendant folder
|
|
payload := fmt.Sprintf(`{ "uid": "%s", "title": "Folder-%d", "parentUid": "%s"}`, descUID, i, parentUID)
|
|
resp := apis.DoRequest(helper, apis.RequestParams{
|
|
User: helper.Org1.Admin,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(payload),
|
|
}, &dtos.Folder{})
|
|
require.Equal(t, http.StatusOK, resp.Response.StatusCode)
|
|
|
|
// Update permissions to use unique descUID where needed
|
|
permissions := tc.permissions
|
|
for j := range permissions {
|
|
if permissions[j].ResourceID == "descuid" {
|
|
permissions[j].ResourceID = descUID
|
|
}
|
|
}
|
|
|
|
user := helper.CreateUser(userLogin, apis.Org1, org.RoleNone, permissions)
|
|
|
|
// Get user ID for cleanup
|
|
userID, err := user.Identity.GetInternalID()
|
|
require.NoError(t, err)
|
|
|
|
// Register cleanup to delete created resources
|
|
t.Cleanup(helper.CleanupTestResources([]string{descUID, parentUID}, []int64{userID}))
|
|
|
|
// Adjust expected UIDs and titles
|
|
expectedParentUIDs := tc.expectedParentUIDs
|
|
expectedParentTitles := tc.expectedParentTitles
|
|
if len(expectedParentUIDs) > 0 {
|
|
expectedParentUIDs = []string{parentUID}
|
|
expectedParentTitles = []string{parentTitle}
|
|
}
|
|
|
|
// Get with accesscontrol disabled
|
|
getResp := apis.DoRequest(helper, apis.RequestParams{
|
|
User: user,
|
|
Method: http.MethodGet,
|
|
Path: "/api/folders/" + descUID,
|
|
}, &dtos.Folder{})
|
|
require.Equal(t, tc.expectedCode, getResp.Response.StatusCode)
|
|
|
|
if tc.expectedCode == http.StatusOK {
|
|
require.NotNil(t, getResp.Result)
|
|
require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersRead])
|
|
require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersWrite])
|
|
|
|
parents := getResp.Result.Parents
|
|
require.Equal(t, len(expectedParentUIDs), len(parents))
|
|
require.Equal(t, len(expectedParentTitles), len(parents))
|
|
for j := 0; j < len(expectedParentUIDs); j++ {
|
|
require.Equal(t, expectedParentUIDs[j], parents[j].UID)
|
|
require.Equal(t, expectedParentTitles[j], parents[j].Title)
|
|
}
|
|
|
|
// Get with accesscontrol enabled
|
|
if tc.checkAccessControl {
|
|
acPerms := []resourcepermissions.SetResourcePermissionCommand{
|
|
{
|
|
Actions: []string{dashboards.ActionFoldersRead},
|
|
Resource: "folders",
|
|
ResourceAttribute: "uid",
|
|
ResourceID: "*",
|
|
},
|
|
{
|
|
Actions: []string{dashboards.ActionFoldersWrite},
|
|
Resource: "folders",
|
|
ResourceAttribute: "uid",
|
|
ResourceID: parentUID,
|
|
},
|
|
}
|
|
acUser := helper.CreateUser(acUserLogin, apis.Org1, org.RoleNone, acPerms)
|
|
|
|
// Get user ID for cleanup
|
|
acUserID, err := acUser.Identity.GetInternalID()
|
|
require.NoError(t, err)
|
|
t.Cleanup(helper.CleanupTestResources([]string{}, []int64{acUserID}))
|
|
|
|
getWithAC := apis.DoRequest(helper, apis.RequestParams{
|
|
User: acUser,
|
|
Method: http.MethodGet,
|
|
Path: "/api/folders/" + descUID + "?accesscontrol=true",
|
|
}, &dtos.Folder{})
|
|
require.Equal(t, tc.expectedCode, getWithAC.Response.StatusCode)
|
|
require.NotNil(t, getWithAC.Result)
|
|
|
|
require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersRead])
|
|
require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersWrite])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFoldersCreateAPIEndpointK8S is the counterpart of pkg/api/folder_test.go TestFoldersCreateAPIEndpoint
|
|
func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
if !db.IsTestDbSQLite() {
|
|
t.Skip("test only on sqlite for now")
|
|
}
|
|
|
|
folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}"
|
|
folderWithTitleEmpty := "{ \"title\": \"\"}"
|
|
folderWithInvalidUid := "{ \"uid\": \"::::::::::::\", \"title\": \"Another folder\"}"
|
|
folderWithUIDTooLong := "{ \"uid\": \"asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm\", \"title\": \"Third folder\"}"
|
|
|
|
type testCase struct {
|
|
description string
|
|
expectedCode int
|
|
expectedMessage string
|
|
expectedFolderSvcError error
|
|
permissions []resourcepermissions.SetResourcePermissionCommand
|
|
input string
|
|
createSecondRecord bool
|
|
}
|
|
|
|
folderCreatePermission := []resourcepermissions.SetResourcePermissionCommand{
|
|
{
|
|
Actions: []string{"folders:create"},
|
|
Resource: "folders",
|
|
ResourceAttribute: "uid",
|
|
ResourceID: "*",
|
|
},
|
|
}
|
|
|
|
// NOTE: folder creation does not return ErrFolderAccessDenied neither ErrFolderNotFound
|
|
tcs := []testCase{
|
|
{
|
|
description: "folder creation succeeds given the correct request for creating a folder",
|
|
input: folderWithoutParentInput,
|
|
expectedCode: http.StatusOK,
|
|
permissions: folderCreatePermission,
|
|
},
|
|
{
|
|
description: "folder creation fails without permissions to create a folder",
|
|
input: folderWithoutParentInput,
|
|
expectedCode: http.StatusForbidden,
|
|
expectedMessage: fmt.Sprintf("You'll need additional permissions to perform this action. Permissions needed: %s", "folders:create"),
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{},
|
|
},
|
|
{
|
|
description: "folder creation fails given folder service error %s",
|
|
input: folderWithTitleEmpty,
|
|
expectedCode: http.StatusBadRequest,
|
|
expectedMessage: dashboards.ErrFolderTitleEmpty.Error(),
|
|
expectedFolderSvcError: dashboards.ErrFolderTitleEmpty,
|
|
permissions: folderCreatePermission,
|
|
},
|
|
{
|
|
description: "folder creation fails given folder service error %s",
|
|
input: folderWithInvalidUid,
|
|
expectedCode: http.StatusBadRequest,
|
|
expectedMessage: dashboards.ErrDashboardInvalidUid.Error(),
|
|
expectedFolderSvcError: dashboards.ErrDashboardInvalidUid,
|
|
permissions: folderCreatePermission,
|
|
},
|
|
{
|
|
description: "folder creation fails given folder service error %s",
|
|
input: folderWithUIDTooLong,
|
|
expectedCode: http.StatusBadRequest,
|
|
expectedMessage: dashboards.ErrDashboardUidTooLong.Error(),
|
|
expectedFolderSvcError: dashboards.ErrDashboardUidTooLong,
|
|
permissions: folderCreatePermission,
|
|
},
|
|
{
|
|
description: "folder creation fails given folder service error %s",
|
|
input: folderWithoutParentInput,
|
|
expectedCode: http.StatusPreconditionFailed,
|
|
expectedMessage: dashboards.ErrFolderVersionMismatch.Error(),
|
|
expectedFolderSvcError: dashboards.ErrFolderVersionMismatch,
|
|
createSecondRecord: true,
|
|
permissions: folderCreatePermission,
|
|
},
|
|
}
|
|
|
|
// test on all dualwriter modes
|
|
for mode := 0; mode <= 4; mode++ {
|
|
modeDw := grafanarest.DualWriterMode(mode)
|
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
})
|
|
for i, tc := range tcs {
|
|
t.Run(fmt.Sprintf("[Mode: %v] "+testDescription(tc.description, tc.expectedFolderSvcError), mode), func(t *testing.T) {
|
|
username := fmt.Sprintf("user-%d", i)
|
|
folderUID := fmt.Sprintf("uid-%d", i)
|
|
|
|
// Update input to use unique UIDs
|
|
input := strings.ReplaceAll(tc.input, `"uid": "uid"`, fmt.Sprintf(`"uid": "%s"`, folderUID))
|
|
|
|
userTest := helper.CreateUser(username, apis.Org1, org.RoleViewer, tc.permissions)
|
|
|
|
if tc.createSecondRecord {
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
create2 := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(input),
|
|
}, &folder.Folder{})
|
|
require.NotEmpty(t, create2.Response)
|
|
require.Equal(t, http.StatusOK, create2.Response.StatusCode)
|
|
}
|
|
|
|
addr := helper.GetEnv().Server.HTTPServer.Listener.Addr()
|
|
login := userTest.Identity.GetLogin()
|
|
baseUrl := fmt.Sprintf("http://%s:%s@%s", login, user.Password(username), addr)
|
|
|
|
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf(
|
|
"%s%s",
|
|
baseUrl,
|
|
"/api/folders",
|
|
), bytes.NewBuffer([]byte(input)))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, tc.expectedCode, resp.StatusCode)
|
|
|
|
type folderWithMessage struct {
|
|
dtos.Folder
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
folder := folderWithMessage{}
|
|
err = json.NewDecoder(resp.Body).Decode(&folder)
|
|
require.NoError(t, err)
|
|
require.NoError(t, resp.Body.Close())
|
|
|
|
if tc.expectedCode == http.StatusOK {
|
|
require.Equal(t, folderUID, folder.UID)
|
|
require.Equal(t, "Folder", folder.Title)
|
|
}
|
|
|
|
if tc.expectedMessage != "" {
|
|
require.Equal(t, tc.expectedMessage, folder.Message)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func testDescription(description string, expectedErr error) string {
|
|
if expectedErr != nil {
|
|
return fmt.Sprintf(description, expectedErr.Error())
|
|
} else {
|
|
return description
|
|
}
|
|
}
|
|
|
|
// There are no counterpart of TestFoldersGetAPIEndpointK8S in pkg/api/folder_test.go
|
|
func TestIntegrationFoldersGetAPIEndpointK8S(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
if !db.IsTestDbSQLite() {
|
|
t.Skip("test only on sqlite for now")
|
|
}
|
|
|
|
type testCase struct {
|
|
description string
|
|
expectedCode int
|
|
params string
|
|
createFolders []string
|
|
expectedOutput []dtos.FolderSearchHit
|
|
permissions []resourcepermissions.SetResourcePermissionCommand
|
|
requestToAnotherOrg bool
|
|
}
|
|
|
|
folderReadAndCreatePermission := []resourcepermissions.SetResourcePermissionCommand{
|
|
{
|
|
Actions: []string{"folders:create", "folders:read"},
|
|
Resource: "folders",
|
|
ResourceAttribute: "uid",
|
|
ResourceID: "*",
|
|
},
|
|
}
|
|
|
|
folder1 := "{ \"uid\": \"foo\", \"title\": \"Folder 1\"}"
|
|
folder2 := "{ \"uid\": \"bar\", \"title\": \"Folder 2\", \"parentUid\": \"foo\"}"
|
|
folder3 := "{ \"uid\": \"qux\", \"title\": \"Folder 3\"}"
|
|
|
|
tcs := []testCase{
|
|
{
|
|
description: "listing folders at root level succeeds",
|
|
createFolders: []string{
|
|
folder1,
|
|
folder2,
|
|
folder3,
|
|
},
|
|
expectedCode: http.StatusOK,
|
|
expectedOutput: []dtos.FolderSearchHit{
|
|
{UID: "foo", Title: "Folder 1"},
|
|
{UID: "qux", Title: "Folder 3"},
|
|
{UID: folder.SharedWithMeFolder.UID, Title: folder.SharedWithMeFolder.Title},
|
|
},
|
|
permissions: folderReadAndCreatePermission,
|
|
},
|
|
{
|
|
description: "listing subfolders succeeds",
|
|
createFolders: []string{
|
|
folder1,
|
|
folder2,
|
|
folder3,
|
|
},
|
|
params: "?parentUid=foo",
|
|
expectedCode: http.StatusOK,
|
|
expectedOutput: []dtos.FolderSearchHit{
|
|
{UID: "bar", Title: "Folder 2", ParentUID: "foo"},
|
|
},
|
|
permissions: folderReadAndCreatePermission,
|
|
},
|
|
{
|
|
description: "listing subfolders for a parent that does not exists",
|
|
createFolders: []string{
|
|
folder1,
|
|
folder2,
|
|
folder3,
|
|
},
|
|
params: "?parentUid=notexists",
|
|
expectedCode: http.StatusNotFound,
|
|
expectedOutput: []dtos.FolderSearchHit{},
|
|
permissions: folderReadAndCreatePermission,
|
|
},
|
|
{
|
|
description: "listing folders at root level fails without the right permissions",
|
|
createFolders: []string{
|
|
folder1,
|
|
folder2,
|
|
folder3,
|
|
},
|
|
params: "?parentUid=notfound",
|
|
expectedCode: http.StatusForbidden,
|
|
expectedOutput: []dtos.FolderSearchHit{},
|
|
permissions: folderReadAndCreatePermission,
|
|
requestToAnotherOrg: true,
|
|
},
|
|
}
|
|
|
|
for mode := 0; mode <= 4; mode++ {
|
|
t.Run(fmt.Sprintf("Mode_%d", mode), func(t *testing.T) {
|
|
modeDw := grafanarest.DualWriterMode(mode)
|
|
|
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
UnifiedStorageEnableSearch: true,
|
|
})
|
|
|
|
// Run all test cases within the same server instance
|
|
for i, tc := range tcs {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
// Use unique UIDs for folders to avoid conflicts between test cases
|
|
userTest := helper.CreateUser(fmt.Sprintf("user-%d", i), apis.Org1, org.RoleNone, tc.permissions)
|
|
|
|
// Create folders with unique UIDs per test case
|
|
for _, f := range tc.createFolders {
|
|
// Replace hardcoded UIDs with unique ones
|
|
uniqueFolder := f
|
|
uniqueFolder = strings.Replace(uniqueFolder, `"foo"`, fmt.Sprintf(`"foo-%d"`, i), 1)
|
|
uniqueFolder = strings.Replace(uniqueFolder, `"bar"`, fmt.Sprintf(`"bar-%d"`, i), 1)
|
|
uniqueFolder = strings.Replace(uniqueFolder, `"qux"`, fmt.Sprintf(`"qux-%d"`, i), 1)
|
|
uniqueFolder = strings.Replace(uniqueFolder, `"parentUid": "foo"`, fmt.Sprintf(`"parentUid": "foo-%d"`, i), 1)
|
|
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: userTest,
|
|
GVR: gvr,
|
|
})
|
|
create2 := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(uniqueFolder),
|
|
}, &folder.Folder{})
|
|
require.NotEmpty(t, create2.Response)
|
|
require.Equal(t, http.StatusOK, create2.Response.StatusCode)
|
|
}
|
|
|
|
addr := helper.GetEnv().Server.HTTPServer.Listener.Addr()
|
|
login := userTest.Identity.GetLogin()
|
|
baseUrl := fmt.Sprintf("http://%s:%s@%s", login, user.Password(fmt.Sprintf("user-%d", i)), addr)
|
|
|
|
// Adjust params with unique UIDs
|
|
params := tc.params
|
|
params = strings.ReplaceAll(params, "foo", fmt.Sprintf("foo-%d", i))
|
|
|
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(
|
|
"%s%s",
|
|
baseUrl,
|
|
fmt.Sprintf("/api/folders%s", params),
|
|
), nil)
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if tc.requestToAnotherOrg {
|
|
req.Header.Set("x-grafana-org-id", "2")
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, tc.expectedCode, resp.StatusCode)
|
|
|
|
if tc.expectedCode == http.StatusOK {
|
|
list := []dtos.FolderSearchHit{}
|
|
err = json.NewDecoder(resp.Body).Decode(&list)
|
|
require.NoError(t, err)
|
|
require.NoError(t, resp.Body.Close())
|
|
|
|
// Adjust expected output with unique UIDs
|
|
expectedOutput := make([]dtos.FolderSearchHit, len(tc.expectedOutput))
|
|
for j, output := range tc.expectedOutput {
|
|
expectedOutput[j] = output
|
|
expectedOutput[j].ID = 0 // ignore IDs
|
|
switch output.UID {
|
|
case "foo":
|
|
expectedOutput[j].UID = fmt.Sprintf("foo-%d", i)
|
|
case "bar":
|
|
expectedOutput[j].UID = fmt.Sprintf("bar-%d", i)
|
|
expectedOutput[j].ParentUID = fmt.Sprintf("foo-%d", i)
|
|
case "qux":
|
|
expectedOutput[j].UID = fmt.Sprintf("qux-%d", i)
|
|
}
|
|
}
|
|
|
|
// ignore IDs in actual list
|
|
for j := 0; j < len(list); j++ {
|
|
list[j].ID = 0
|
|
}
|
|
|
|
require.ElementsMatch(t, expectedOutput, list)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Reproduces a bug where folder deletion does not check for attached library panels.
|
|
func TestIntegrationFolderDeletionBlockedByLibraryElements(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("with dual write (unified storage, mode %v, delete blocked by library elements)", grafanarest.DualWriterMode(mode)), func(t *testing.T) {
|
|
modeDw := grafanarest.DualWriterMode(mode)
|
|
|
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
EnableFeatureToggles: []string{
|
|
featuremgmt.FlagKubernetesLibraryPanels,
|
|
},
|
|
UnifiedStorageEnableSearch: true,
|
|
})
|
|
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
// Create a folder via legacy API (/api/folders) so it is visible to both paths
|
|
folderUID := fmt.Sprintf("libpanel-del-%d", mode)
|
|
legacyPayload := fmt.Sprintf(`{
|
|
"title": "Folder With Library Panel %d",
|
|
"uid": "%s"
|
|
}`, mode, folderUID)
|
|
|
|
legacyCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(legacyPayload),
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, legacyCreate.Result)
|
|
require.Equal(t, folderUID, legacyCreate.Result.UID)
|
|
|
|
// Create a library element inside the folder via /api to simulate an attached library panel
|
|
libElementPayload := fmt.Sprintf(`{
|
|
"kind": 1,
|
|
"name": "LP in %s",
|
|
"folderUid": "%s",
|
|
"model": {
|
|
"type": "text",
|
|
"title": "LP in %s"
|
|
}
|
|
}`, folderUID, folderUID, folderUID)
|
|
|
|
libCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/library-elements",
|
|
Body: []byte(libElementPayload),
|
|
}, &struct{}{})
|
|
require.NotNil(t, libCreate.Response)
|
|
require.Equal(t, http.StatusOK, libCreate.Response.StatusCode)
|
|
|
|
// Attempt to delete the folder via K8s API. This should be blocked (ErrFolderNotEmpty)
|
|
err := client.Resource.Delete(context.Background(), folderUID, metav1.DeleteOptions{})
|
|
require.Error(t, err, "expected folder deletion to be blocked when library panels exist")
|
|
|
|
// Verify the folder still exists
|
|
_, getErr := client.Resource.Get(context.Background(), folderUID, metav1.GetOptions{})
|
|
require.NoError(t, getErr, "folder should still exist after failed deletion")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIntegrationRootFolderDeletionBlockedByLibraryElementsInSubfolder(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
if !db.IsTestDbSQLite() {
|
|
t.Skip("test only on sqlite for now")
|
|
}
|
|
|
|
// TODO: re-enable on mode 4 and 5 when we migrate /api to /apis for library connections, and begin to
|
|
// use search to return the connections, rather than the connections table.
|
|
for mode := 0; mode <= 3; mode++ {
|
|
t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, delete parent blocked by library elements in child)", grafanarest.DualWriterMode(mode)), func(t *testing.T) {
|
|
modeDw := grafanarest.DualWriterMode(mode)
|
|
|
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
EnableFeatureToggles: []string{
|
|
featuremgmt.FlagKubernetesLibraryPanels,
|
|
},
|
|
UnifiedStorageEnableSearch: true,
|
|
})
|
|
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
parentUID := fmt.Sprintf("libpanel-parent-%d", mode)
|
|
parentPayload := fmt.Sprintf(`{
|
|
"title": "Parent Folder %d",
|
|
"uid": "%s"
|
|
}`, mode, parentUID)
|
|
parentCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(parentPayload),
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, parentCreate.Result)
|
|
require.Equal(t, parentUID, parentCreate.Result.UID)
|
|
|
|
childUID := fmt.Sprintf("libpanel-child-%d", mode)
|
|
childPayload := fmt.Sprintf(`{
|
|
"title": "Child Folder %d",
|
|
"uid": "%s",
|
|
"parentUid": "%s"
|
|
}`, mode, childUID, parentUID)
|
|
childCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(childPayload),
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, childCreate.Result)
|
|
require.Equal(t, childUID, childCreate.Result.UID)
|
|
require.Equal(t, parentUID, childCreate.Result.ParentUID)
|
|
|
|
libElementPayload := fmt.Sprintf(`{
|
|
"kind": 1,
|
|
"name": "LP in %s",
|
|
"folderUid": "%s",
|
|
"model": {
|
|
"type": "text",
|
|
"title": "LP in %s"
|
|
}
|
|
}`, childUID, childUID, childUID)
|
|
|
|
libCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/library-elements",
|
|
Body: []byte(libElementPayload),
|
|
}, &struct{}{})
|
|
require.NotNil(t, libCreate.Response)
|
|
require.Equal(t, http.StatusOK, libCreate.Response.StatusCode)
|
|
|
|
// Attempt to delete the parent folder; should be blocked because child folder contains a library panel
|
|
err := client.Resource.Delete(context.Background(), parentUID, metav1.DeleteOptions{})
|
|
require.Error(t, err, "expected parent folder deletion to be blocked when child contains library panels")
|
|
|
|
// Verify both folders still exist
|
|
_, getParentErr := client.Resource.Get(context.Background(), parentUID, metav1.GetOptions{})
|
|
require.NoError(t, getParentErr, "parent folder should still exist after failed deletion")
|
|
_, getChildErr := client.Resource.Get(context.Background(), childUID, metav1.GetOptions{})
|
|
require.NoError(t, getChildErr, "child folder should still exist after failed deletion")
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test folder deletion with connected (in-use) library panels - should be blocked
|
|
func TestIntegrationFolderDeletionBlockedByConnectedLibraryPanels(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
if !db.IsTestDbSQLite() {
|
|
t.Skip("test only on sqlite for now")
|
|
}
|
|
|
|
// TODO: re-enable on mode 4 and 5 when we migrate /api to /apis for library connections, and begin to
|
|
// use search to return the connections, rather than the connections table.
|
|
for mode := 0; mode <= 3; mode++ {
|
|
t.Run(fmt.Sprintf("mode %v - delete blocked by connected library panels in folder and subfolder", grafanarest.DualWriterMode(mode)), func(t *testing.T) {
|
|
modeDw := grafanarest.DualWriterMode(mode)
|
|
|
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
"dashboards.dashboard.grafana.app": {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
UnifiedStorageEnableSearch: true,
|
|
})
|
|
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
// Create parent and child folders
|
|
uid := uuid.NewString()[:8]
|
|
parentUID := fmt.Sprintf("connected-parent-%d-%s", mode, uid)
|
|
childUID := fmt.Sprintf("connected-child-%d-%s", mode, uid)
|
|
createTestFolder(t, helper, client, parentUID, fmt.Sprintf("Parent Folder %d-%s", mode, uid), "")
|
|
createTestFolder(t, helper, client, childUID, fmt.Sprintf("Child Folder %d-%s", mode, uid), parentUID)
|
|
|
|
// Create library panels in both folders
|
|
parentLibPanelName := fmt.Sprintf("Connected LP in parent %d-%s", mode, uid)
|
|
childLibPanelName := fmt.Sprintf("Connected LP in child %d-%s", mode, uid)
|
|
parentLibPanelUID := createTestLibraryPanel(t, helper, client, parentLibPanelName, parentUID)
|
|
childLibPanelUID := createTestLibraryPanel(t, helper, client, childLibPanelName, childUID)
|
|
|
|
// Create dashboards using library panels (makes them connected)
|
|
parentDashUID := createDashboardWithLibraryPanel(t, helper, client,
|
|
fmt.Sprintf("Dashboard with LP in parent %d-%s", mode, uid),
|
|
parentLibPanelUID, "Connected LP in parent", parentUID)
|
|
childDashUID := createDashboardWithLibraryPanel(t, helper, client,
|
|
fmt.Sprintf("Dashboard with LP in child %d-%s", mode, uid),
|
|
childLibPanelUID, "Connected LP in child", childUID)
|
|
|
|
// Attempt to delete the parent folder - should be blocked because library panels are connected
|
|
parentDelete := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodDelete,
|
|
Path: "/api/folders/" + parentUID,
|
|
}, &folder.Folder{})
|
|
require.Equal(t, http.StatusForbidden, parentDelete.Response.StatusCode)
|
|
|
|
// Verify both folders still exist
|
|
_, getParentErr := client.Resource.Get(context.Background(), parentUID, metav1.GetOptions{})
|
|
require.NoError(t, getParentErr, "parent folder should still exist after failed deletion")
|
|
_, getChildErr := client.Resource.Get(context.Background(), childUID, metav1.GetOptions{})
|
|
require.NoError(t, getChildErr, "child folder should still exist after failed deletion")
|
|
|
|
// Verify library panels still exist
|
|
verifyLibraryPanelExists(t, helper, client, parentLibPanelUID)
|
|
verifyLibraryPanelExists(t, helper, client, childLibPanelUID)
|
|
|
|
// Verify dashboards still exist
|
|
verifyDashboardExists(t, helper, client, parentDashUID)
|
|
verifyDashboardExists(t, helper, client, childDashUID)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test folder deletion with dangling (unconnected) library panels - should succeed and clean up
|
|
func TestIntegrationFolderDeletionWithDanglingLibraryPanels(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 %v - delete succeeds and cleans up dangling library panels in folder and subfolder", grafanarest.DualWriterMode(mode)), func(t *testing.T) {
|
|
modeDw := grafanarest.DualWriterMode(mode)
|
|
|
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
UnifiedStorageEnableSearch: true,
|
|
})
|
|
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
// Create parent and child folders
|
|
uid := uuid.NewString()[:8]
|
|
parentUID := fmt.Sprintf("dangling-parent-%d-%s", mode, uid)
|
|
childUID := fmt.Sprintf("dangling-child-%d-%s", mode, uid)
|
|
createTestFolder(t, helper, client, parentUID, fmt.Sprintf("Parent Folder %d-%s", mode, uid), "")
|
|
createTestFolder(t, helper, client, childUID, fmt.Sprintf("Child Folder %d-%s", mode, uid), parentUID)
|
|
|
|
// Create dangling library panels in both folders (not connected to any dashboard)
|
|
parentLibPanelUID := createTestLibraryPanel(t, helper, client,
|
|
fmt.Sprintf("Dangling LP in parent %d-%s", mode, uid), parentUID)
|
|
childLibPanelUID := createTestLibraryPanel(t, helper, client,
|
|
fmt.Sprintf("Dangling LP in child %d-%s", mode, uid), childUID)
|
|
|
|
// Verify library panels exist before deletion
|
|
verifyLibraryPanelExists(t, helper, client, parentLibPanelUID)
|
|
verifyLibraryPanelExists(t, helper, client, childLibPanelUID)
|
|
|
|
// Attempt to delete the parent folder - should be blocked because library panels are connected
|
|
parentDelete := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodDelete,
|
|
Path: "/api/folders/" + parentUID,
|
|
}, &folder.Folder{})
|
|
require.Equal(t, http.StatusOK, parentDelete.Response.StatusCode, parentDelete.Body)
|
|
// Verify folders are deleted
|
|
_, getParentErr := client.Resource.Get(context.Background(), parentUID, metav1.GetOptions{})
|
|
require.Error(t, getParentErr, "parent folder should not exist after deletion")
|
|
_, getChildErr := client.Resource.Get(context.Background(), childUID, metav1.GetOptions{})
|
|
require.Error(t, getChildErr, "child folder should not exist after deletion")
|
|
|
|
// Verify dangling library panels were cleaned up
|
|
verifyLibraryPanelDeleted(t, helper, client, parentLibPanelUID, "dangling library panel in parent should be deleted")
|
|
verifyLibraryPanelDeleted(t, helper, client, childLibPanelUID, "dangling library panel in child should be deleted")
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper function to create a folder with specified UID and optional parent
|
|
func createTestFolder(t *testing.T, helper *apis.K8sTestHelper, client *apis.K8sResourceClient, uid, title, parentUID string) *folder.Folder {
|
|
t.Helper()
|
|
|
|
payload := fmt.Sprintf(`{
|
|
"title": "%s",
|
|
"uid": "%s"`, title, uid)
|
|
|
|
if parentUID != "" {
|
|
payload += fmt.Sprintf(`,
|
|
"parentUid": "%s"`, parentUID)
|
|
}
|
|
|
|
payload += "}"
|
|
|
|
folderCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(payload),
|
|
}, &folder.Folder{})
|
|
|
|
require.NotNil(t, folderCreate.Result)
|
|
require.Equal(t, uid, folderCreate.Result.UID)
|
|
|
|
return folderCreate.Result
|
|
}
|
|
|
|
// Helper function to create a library panel in a folder
|
|
func createTestLibraryPanel(t *testing.T, helper *apis.K8sTestHelper, client *apis.K8sResourceClient, name, folderUID string) string {
|
|
t.Helper()
|
|
|
|
libPanelPayload := fmt.Sprintf(`{
|
|
"kind": 1,
|
|
"name": "%s",
|
|
"folderUid": "%s",
|
|
"model": {
|
|
"type": "text",
|
|
"title": "%s"
|
|
}
|
|
}`, name, folderUID, name)
|
|
|
|
libCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/library-elements",
|
|
Body: []byte(libPanelPayload),
|
|
}, &map[string]interface{}{})
|
|
|
|
require.NotNil(t, libCreate.Response)
|
|
require.Equal(t, http.StatusOK, libCreate.Response.StatusCode)
|
|
|
|
libPanelUID := (*libCreate.Result)["result"].(map[string]interface{})["uid"].(string)
|
|
require.NotEmpty(t, libPanelUID)
|
|
|
|
return libPanelUID
|
|
}
|
|
|
|
// Helper function to create a dashboard that uses a library panel
|
|
func createDashboardWithLibraryPanel(t *testing.T, helper *apis.K8sTestHelper, client *apis.K8sResourceClient, dashTitle, libPanelUID, libPanelName, folderUID string) string {
|
|
t.Helper()
|
|
|
|
dashPayload := fmt.Sprintf(`{
|
|
"dashboard": {
|
|
"title": "%s",
|
|
"panels": [{
|
|
"id": 1,
|
|
"libraryPanel": {
|
|
"uid": "%s",
|
|
"name": "%s"
|
|
}
|
|
}]
|
|
},
|
|
"folderUid": "%s",
|
|
"overwrite": false
|
|
}`, dashTitle, libPanelUID, libPanelName, folderUID)
|
|
|
|
dashCreate := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/dashboards/db",
|
|
Body: []byte(dashPayload),
|
|
}, &map[string]interface{}{})
|
|
|
|
require.NotNil(t, dashCreate.Response)
|
|
require.Equal(t, http.StatusOK, dashCreate.Response.StatusCode)
|
|
|
|
// Extract dashboard UID from response
|
|
dashUID := (*dashCreate.Result)["uid"].(string)
|
|
require.NotEmpty(t, dashUID)
|
|
|
|
return dashUID
|
|
}
|
|
|
|
// Helper function to verify library panel exists
|
|
func verifyLibraryPanelExists(t *testing.T, helper *apis.K8sTestHelper, client *apis.K8sResourceClient, libPanelUID string) {
|
|
t.Helper()
|
|
|
|
libGet := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodGet,
|
|
Path: fmt.Sprintf("/api/library-elements/%s", libPanelUID),
|
|
}, &map[string]interface{}{})
|
|
|
|
require.Equal(t, http.StatusOK, libGet.Response.StatusCode)
|
|
}
|
|
|
|
// Helper function to verify library panel does not exist
|
|
func verifyLibraryPanelDeleted(t *testing.T, helper *apis.K8sTestHelper, client *apis.K8sResourceClient, libPanelUID, message string) {
|
|
t.Helper()
|
|
|
|
libGet := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodGet,
|
|
Path: fmt.Sprintf("/api/library-elements/%s", libPanelUID),
|
|
}, &map[string]interface{}{})
|
|
|
|
require.Equal(t, http.StatusNotFound, libGet.Response.StatusCode, message)
|
|
}
|
|
|
|
// Helper function to verify dashboard exists by UID
|
|
func verifyDashboardExists(t *testing.T, helper *apis.K8sTestHelper, client *apis.K8sResourceClient, dashUID string) {
|
|
t.Helper()
|
|
|
|
dashGet := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodGet,
|
|
Path: fmt.Sprintf("/api/dashboards/uid/%s", dashUID),
|
|
}, &map[string]interface{}{})
|
|
|
|
require.Equal(t, http.StatusOK, dashGet.Response.StatusCode, fmt.Sprintf("dashboard %s should still exist", dashUID))
|
|
}
|
|
|
|
// Test moving folders to root.
|
|
func TestIntegrationMoveNestedFolderToRootK8S(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
if !db.IsTestDbSQLite() {
|
|
t.Skip("test only on sqlite for now")
|
|
}
|
|
|
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: grafanarest.Mode5,
|
|
},
|
|
},
|
|
UnifiedStorageEnableSearch: true,
|
|
})
|
|
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
// Create f1 under root
|
|
f1Payload := `{"title":"Folder 1","uid":"f1"}`
|
|
createF1 := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(f1Payload),
|
|
}, &dtos.Folder{})
|
|
require.NotNil(t, createF1.Result)
|
|
require.Equal(t, http.StatusOK, createF1.Response.StatusCode)
|
|
require.Equal(t, "f1", createF1.Result.UID)
|
|
require.Equal(t, "", createF1.Result.ParentUID)
|
|
|
|
// Create f2 under f1
|
|
f2Payload := `{"title":"Folder 2","uid":"f2","parentUid":"f1"}`
|
|
createF2 := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(f2Payload),
|
|
}, &dtos.Folder{})
|
|
require.NotNil(t, createF2.Result)
|
|
require.Equal(t, http.StatusOK, createF2.Response.StatusCode)
|
|
require.Equal(t, "f2", createF2.Result.UID)
|
|
require.Equal(t, "f1", createF2.Result.ParentUID)
|
|
|
|
// Move f2 to the root by having parentUid being empty in the request body
|
|
move := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders/f2/move",
|
|
Body: []byte(`{"parentUid":""}`),
|
|
}, &dtos.Folder{})
|
|
require.NotNil(t, move.Result)
|
|
require.Equal(t, http.StatusOK, move.Response.StatusCode)
|
|
require.Equal(t, "f2", move.Result.UID)
|
|
require.Equal(t, "", move.Result.ParentUID)
|
|
|
|
// Fetch the folder to confirm it is now at root
|
|
get := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodGet,
|
|
Path: "/api/folders/f2",
|
|
}, &dtos.Folder{})
|
|
require.NotNil(t, get.Result)
|
|
require.Equal(t, http.StatusOK, get.Response.StatusCode)
|
|
require.Equal(t, "f2", get.Result.UID)
|
|
require.Equal(t, "", get.Result.ParentUID)
|
|
|
|
// Check that we can get the same folder using metadata.name selector
|
|
results, err := client.Resource.List(context.Background(), metav1.ListOptions{
|
|
FieldSelector: "metadata.name=" + get.Result.UID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, results.Items, 1)
|
|
require.Equal(t, "f2", results.Items[0].GetName())
|
|
}
|
|
|
|
// Test deleting nested folders ensures postorder deletion
|
|
func TestIntegrationDeleteNestedFoldersPostorder(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 nested folder hierarchy in postorder", mode), func(t *testing.T) {
|
|
modeDw := grafanarest.DualWriterMode(mode)
|
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: true,
|
|
DisableAnonymous: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: modeDw,
|
|
},
|
|
},
|
|
UnifiedStorageEnableSearch: true,
|
|
})
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
// Helper function to create a folder and return its UID and ParentUID
|
|
createFolder := func(title, uid, parentUid string) (string, string) {
|
|
payload := fmt.Sprintf(`{"title":"%s","uid":"%s"%s}`, title, uid, func() string {
|
|
if parentUid != "" {
|
|
return fmt.Sprintf(`,"parentUid":"%s"`, parentUid)
|
|
}
|
|
return ""
|
|
}())
|
|
create := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodPost,
|
|
Path: "/api/folders",
|
|
Body: []byte(payload),
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, create.Result)
|
|
require.Equal(t, http.StatusOK, create.Response.StatusCode)
|
|
return create.Result.UID, create.Result.ParentUID
|
|
}
|
|
|
|
// Create a nested folder structure:
|
|
// parent
|
|
// / \
|
|
// child1 child2
|
|
// |
|
|
// grandchild
|
|
|
|
// Create parent folder
|
|
parentUID, _ := createFolder(fmt.Sprintf("Parent-%d", mode), fmt.Sprintf("parent-%d", mode), "")
|
|
|
|
// Create child1 folder
|
|
child1UID, child1ParentUID := createFolder(fmt.Sprintf("Child1-%d", mode), fmt.Sprintf("child1-%d", mode), parentUID)
|
|
require.Equal(t, parentUID, child1ParentUID)
|
|
|
|
// Create child2 folder
|
|
child2UID, child2ParentUID := createFolder(fmt.Sprintf("Child2-%d", mode), fmt.Sprintf("child2-%d", mode), parentUID)
|
|
require.Equal(t, parentUID, child2ParentUID)
|
|
|
|
// Create grandchild folder under child1
|
|
grandchildUID, grandchildParentUID := createFolder(fmt.Sprintf("Grandchild-%d", mode), fmt.Sprintf("grandchild-%d", mode), child1UID)
|
|
require.Equal(t, child1UID, grandchildParentUID)
|
|
|
|
// Verify the structure before deletion
|
|
verifyFolderExists := func(uid string, shouldExist bool) {
|
|
_, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{})
|
|
if shouldExist {
|
|
require.NoError(t, err, "folder %s should exist", uid)
|
|
} else {
|
|
require.Error(t, err, "folder %s should not exist", uid)
|
|
}
|
|
}
|
|
|
|
// All folders should exist
|
|
verifyFolderExists(parentUID, true)
|
|
verifyFolderExists(child1UID, true)
|
|
verifyFolderExists(child2UID, true)
|
|
verifyFolderExists(grandchildUID, true)
|
|
|
|
// Delete the parent folder - this should trigger postorder deletion
|
|
parentDelete := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodDelete,
|
|
Path: "/api/folders/" + parentUID,
|
|
}, &folder.Folder{})
|
|
require.NotNil(t, parentDelete.Result)
|
|
require.Equal(t, http.StatusOK, parentDelete.Response.StatusCode)
|
|
|
|
// All folders should now be deleted (postorder deletion: grandchild, child1, child2, parent)
|
|
verifyFolderExists(grandchildUID, false)
|
|
verifyFolderExists(child1UID, false)
|
|
verifyFolderExists(child2UID, false)
|
|
verifyFolderExists(parentUID, false)
|
|
})
|
|
}
|
|
}
|
|
|
|
func setupProvisioningDir(t *testing.T, opts *testinfra.GrafanaOpts) {
|
|
opts.Dir, opts.DirPath = testinfra.CreateGrafDir(t, *opts)
|
|
|
|
// Create provisioning directories
|
|
provDashboardsDir := fmt.Sprintf("%s/conf/provisioning/dashboards", opts.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)
|
|
}
|
|
|
|
// 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,
|
|
},
|
|
}
|
|
|
|
setupProvisioningDir(t, &ops)
|
|
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)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test that folders created during provisioning using the dual writer have the
|
|
// appropriate labels and annotations in unified storage.
|
|
func TestIntegrationProvisionedFolderPropagatesLabelsAndAnnotations(t *testing.T) {
|
|
mode3 := grafanarest.DualWriterMode(3)
|
|
ops := testinfra.GrafanaOpts{
|
|
DisableAnonymous: true,
|
|
AppModeProduction: true,
|
|
APIServerStorageType: "unified",
|
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
|
folders.RESOURCEGROUP: {
|
|
DualWriterMode: mode3,
|
|
},
|
|
"dashboards.dashboard.grafana.app": {
|
|
DualWriterMode: mode3,
|
|
},
|
|
},
|
|
EnableFeatureToggles: []string{
|
|
featuremgmt.FlagUnifiedStorageSearch,
|
|
},
|
|
}
|
|
|
|
setupProvisioningDir(t, &ops)
|
|
helper := apis.NewK8sTestHelper(t, ops)
|
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
|
User: helper.Org1.Admin,
|
|
GVR: gvr,
|
|
})
|
|
|
|
var folderList folders.FolderList
|
|
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
|
resp := apis.DoRequest(helper, apis.RequestParams{
|
|
User: client.Args.User,
|
|
Method: http.MethodGet,
|
|
Path: fmt.Sprintf("/apis/folder.grafana.app/v1beta1/namespaces/%s/folders", client.Args.Namespace),
|
|
}, &map[string]interface{}{})
|
|
require.NotNil(t, resp.Response)
|
|
require.Equal(t, http.StatusOK, resp.Response.StatusCode)
|
|
require.NoError(t, json.Unmarshal(resp.Body, &folderList))
|
|
}, 10*time.Second, 25*time.Millisecond)
|
|
|
|
require.Len(t, folderList.Items, 1)
|
|
|
|
accessor, err := utils.MetaAccessor(&folderList.Items[0])
|
|
require.NoError(t, err)
|
|
|
|
expectedLabels := map[string]string{"grafana.app/deprecatedInternalID": "1"}
|
|
expectedAnnotations := map[string]string{
|
|
"grafana.app/createdBy": "access-policy:service",
|
|
"grafana.app/managedBy": "classic-file-provisioning",
|
|
"grafana.app/managerId": "provisioned dashboards",
|
|
}
|
|
|
|
require.Equal(t, expectedLabels, accessor.GetLabels())
|
|
require.Equal(t, expectedAnnotations, accessor.GetAnnotations())
|
|
}
|