Provisioning: Block Library Panel creation in provisioned folders (#114933)
* WIP: Block Library Panel creation in provisioned folders * blocking patch - adding integration tests * checking code in tests * addressing comments, adding one more test
This commit is contained in:
@@ -424,6 +424,9 @@ func (l *LibraryElementService) toLibraryElementError(err error, message string)
|
|||||||
if errors.Is(err, model.ErrLibraryElementUIDTooLong) {
|
if errors.Is(err, model.ErrLibraryElementUIDTooLong) {
|
||||||
return response.Error(http.StatusBadRequest, model.ErrLibraryElementUIDTooLong.Error(), err)
|
return response.Error(http.StatusBadRequest, model.ErrLibraryElementUIDTooLong.Error(), err)
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, model.ErrLibraryElementProvisionedFolder) {
|
||||||
|
return response.Error(http.StatusConflict, model.ErrLibraryElementProvisionedFolder.Error(), err)
|
||||||
|
}
|
||||||
if err != nil && strings.Contains(err.Error(), "insufficient permissions") {
|
if err != nil && strings.Contains(err.Error(), "insufficient permissions") {
|
||||||
return response.Error(http.StatusForbidden, err.Error(), err)
|
return response.Error(http.StatusForbidden, err.Error(), err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
@@ -125,6 +126,20 @@ func (l *LibraryElementService) CreateElement(c context.Context, signedInUser id
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.FolderUID != nil {
|
||||||
|
f, err := l.folderService.Get(c, &folder.GetFolderQuery{
|
||||||
|
OrgID: signedInUser.GetOrgID(),
|
||||||
|
UID: cmd.FolderUID,
|
||||||
|
SignedInUser: signedInUser,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return model.LibraryElementDTO{}, err
|
||||||
|
}
|
||||||
|
if f.ManagedBy == utils.ManagerKindRepo {
|
||||||
|
return model.LibraryElementDTO{}, model.ErrLibraryElementProvisionedFolder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updatedModel := cmd.Model
|
updatedModel := cmd.Model
|
||||||
var err error
|
var err error
|
||||||
if cmd.Kind == int64(model.PanelElement) {
|
if cmd.Kind == int64(model.PanelElement) {
|
||||||
@@ -601,6 +616,21 @@ func (l *LibraryElementService) PatchLibraryElement(c context.Context, signedInU
|
|||||||
if err := l.requireSupportedElementKind(cmd.Kind); err != nil {
|
if err := l.requireSupportedElementKind(cmd.Kind); err != nil {
|
||||||
return model.LibraryElementDTO{}, err
|
return model.LibraryElementDTO{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.FolderUID != nil {
|
||||||
|
f, err := l.folderService.Get(c, &folder.GetFolderQuery{
|
||||||
|
OrgID: signedInUser.GetOrgID(),
|
||||||
|
UID: cmd.FolderUID,
|
||||||
|
SignedInUser: signedInUser,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return model.LibraryElementDTO{}, err
|
||||||
|
}
|
||||||
|
if f.ManagedBy == utils.ManagerKindRepo {
|
||||||
|
return model.LibraryElementDTO{}, model.ErrLibraryElementProvisionedFolder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
||||||
elementInDB, err := l.GetLibraryElement(c, signedInUser, session, uid)
|
elementInDB, err := l.GetLibraryElement(c, signedInUser, session, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ var (
|
|||||||
ErrLibraryElementInvalidUID = errors.New("uid contains illegal characters")
|
ErrLibraryElementInvalidUID = errors.New("uid contains illegal characters")
|
||||||
// errLibraryElementUIDTooLong is an error for when the uid of a library element is invalid
|
// errLibraryElementUIDTooLong is an error for when the uid of a library element is invalid
|
||||||
ErrLibraryElementUIDTooLong = errors.New("uid too long, max 40 characters")
|
ErrLibraryElementUIDTooLong = errors.New("uid too long, max 40 characters")
|
||||||
|
// ErrLibraryElementProvisionedFolder indicates that a library element cannot be created on a provisioned folder.
|
||||||
|
ErrLibraryElementProvisionedFolder = errors.New("resource type not supported in repository-managed folders")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Commands
|
// Commands
|
||||||
|
|||||||
@@ -957,3 +957,49 @@ func (h *provisioningTestHelper) CleanupAllRepos(t *testing.T) {
|
|||||||
assert.Equal(collect, 0, len(list.Items), "repositories should be cleaned up")
|
assert.Equal(collect, 0, len(list.Items), "repositories should be cleaned up")
|
||||||
}, waitTimeoutDefault, waitIntervalDefault, "repositories should be cleaned up between subtests")
|
}, waitTimeoutDefault, waitIntervalDefault, "repositories should be cleaned up between subtests")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func postHelper(t *testing.T, helper apis.K8sTestHelper, path string, body interface{}, user apis.User) (map[string]interface{}, int, error) {
|
||||||
|
return requestHelper(t, helper, http.MethodPost, path, body, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchHelper(t *testing.T, helper apis.K8sTestHelper, path string, body interface{}, user apis.User) (map[string]interface{}, int, error) {
|
||||||
|
return requestHelper(t, helper, http.MethodPatch, path, body, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestHelper(
|
||||||
|
t *testing.T,
|
||||||
|
helper apis.K8sTestHelper,
|
||||||
|
method string,
|
||||||
|
path string,
|
||||||
|
body interface{},
|
||||||
|
user apis.User,
|
||||||
|
) (map[string]interface{}, int, error) {
|
||||||
|
bodyJSON, err := json.Marshal(body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resp := apis.DoRequest(&helper, apis.RequestParams{
|
||||||
|
User: user,
|
||||||
|
Method: method,
|
||||||
|
Path: path,
|
||||||
|
Body: bodyJSON,
|
||||||
|
ContentType: "application/json",
|
||||||
|
}, &struct{}{})
|
||||||
|
|
||||||
|
if resp.Response.StatusCode != http.StatusOK {
|
||||||
|
res := map[string]interface{}{}
|
||||||
|
err := json.Unmarshal(resp.Body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("failed to unmarshal response JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, resp.Response.StatusCode, fmt.Errorf("failure when making request: %s", resp.Response.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
err = json.Unmarshal(resp.Body, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("failed to unmarshal response JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, resp.Response.StatusCode, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package provisioning
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
foldersV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||||
|
"github.com/grafana/grafana/pkg/util/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// We currently block the creation of library panels in provisioned folders.
|
||||||
|
func TestIntegrationLibraryPanels_ProvisionedFolders(t *testing.T) {
|
||||||
|
testutil.SkipIntegrationTestInShortMode(t)
|
||||||
|
|
||||||
|
helper := runGrafana(t)
|
||||||
|
helper.CreateRepo(t, TestRepo{
|
||||||
|
Name: "test-repo",
|
||||||
|
Target: "folder",
|
||||||
|
ExpectedFolders: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should fail to create library element in provisioned folder", func(t *testing.T) {
|
||||||
|
folders, err := helper.Folders.Resource.List(t.Context(), metav1.ListOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, folders.Items, 1)
|
||||||
|
|
||||||
|
managedFolderName := folders.Items[0].GetName()
|
||||||
|
libraryElement := map[string]interface{}{
|
||||||
|
"kind": 1,
|
||||||
|
"name": "Library Panel",
|
||||||
|
"folderUid": managedFolderName,
|
||||||
|
"model": map[string]interface{}{
|
||||||
|
"type": "text",
|
||||||
|
"title": "Library Panel",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
libraryElementURL := "/api/library-elements"
|
||||||
|
libraryElementData, code, err := postHelper(t, *helper.K8sTestHelper, libraryElementURL, libraryElement, helper.Org1.Admin)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, http.StatusConflict, code)
|
||||||
|
require.NotNil(t, libraryElementData)
|
||||||
|
require.Equal(t, "resource type not supported in repository-managed folders", libraryElementData["message"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should fail to patch library element, moving it in a provisioned folder", func(t *testing.T) {
|
||||||
|
// Getting managed folder
|
||||||
|
folders, err := helper.Folders.Resource.List(t.Context(), metav1.ListOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, folders.Items, 1)
|
||||||
|
managedFolderName := folders.Items[0].GetName()
|
||||||
|
|
||||||
|
unmanagedFolder := &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": foldersV1.FolderResourceInfo.GroupVersion().String(),
|
||||||
|
"kind": foldersV1.FolderResourceInfo.GroupVersionKind().Kind,
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"generateName": "test-folder-",
|
||||||
|
},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"title": "Library Panel",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
createdFolder, err := helper.Folders.Resource.Create(t.Context(), unmanagedFolder, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, createdFolder)
|
||||||
|
|
||||||
|
libraryElement := map[string]interface{}{
|
||||||
|
"kind": 1,
|
||||||
|
"name": "Moved Library Panel",
|
||||||
|
"folderUid": createdFolder.GetName(),
|
||||||
|
"model": map[string]interface{}{
|
||||||
|
"type": "text",
|
||||||
|
"title": "Moved Library Panel",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
libraryElementURL := "/api/library-elements"
|
||||||
|
libraryElementData, code, err := postHelper(t, *helper.K8sTestHelper, libraryElementURL, libraryElement, helper.Org1.Admin)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, code)
|
||||||
|
require.NotNil(t, libraryElementData)
|
||||||
|
|
||||||
|
res := libraryElementData["result"].(map[string]interface{})
|
||||||
|
helper.SetPermissions(helper.Org1.Admin, []resourcepermissions.SetResourcePermissionCommand{
|
||||||
|
{
|
||||||
|
Actions: []string{"library.panels:write"},
|
||||||
|
Resource: "library.panels",
|
||||||
|
ResourceAttribute: "uid",
|
||||||
|
ResourceID: "*",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Patching libraryElement - changing folder to a managed one
|
||||||
|
updatedLibraryElement := map[string]interface{}{
|
||||||
|
"kind": 1,
|
||||||
|
"version": res["version"],
|
||||||
|
"folderUid": managedFolderName,
|
||||||
|
}
|
||||||
|
patchLibraryElementURL := fmt.Sprintf("/api/library-elements/%f", +res["id"].(float64))
|
||||||
|
newLibraryElement, code, err := patchHelper(t, *helper.K8sTestHelper, patchLibraryElementURL, updatedLibraryElement, helper.Org1.Admin)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, http.StatusConflict, code)
|
||||||
|
require.NotNil(t, newLibraryElement)
|
||||||
|
require.Equal(t, "resource type not supported in repository-managed folders", newLibraryElement["message"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegrationLibraryPanels_UnprovisionedFolders(t *testing.T) {
|
||||||
|
const repo = "test-repo"
|
||||||
|
helper := runGrafana(t)
|
||||||
|
helper.CreateRepo(t, TestRepo{
|
||||||
|
Name: repo,
|
||||||
|
Target: "folder",
|
||||||
|
ExpectedFolders: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should create library element when folder is released", func(t *testing.T) {
|
||||||
|
folders, err := helper.Folders.Resource.List(t.Context(), metav1.ListOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, folders.Items, 1)
|
||||||
|
managedFolderName := folders.Items[0].GetName()
|
||||||
|
require.Contains(t, folders.Items[0].GetAnnotations(), utils.AnnoKeyManagerKind, "folder should be managed")
|
||||||
|
require.Contains(t, folders.Items[0].GetAnnotations(), utils.AnnoKeyManagerIdentity, "folder should be managed")
|
||||||
|
|
||||||
|
_, err = helper.Repositories.Resource.Patch(t.Context(), repo, types.JSONPatchType, []byte(`[
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/metadata/finalizers",
|
||||||
|
"value": ["cleanup", "release-orphan-resources"]
|
||||||
|
}
|
||||||
|
]`), metav1.PatchOptions{})
|
||||||
|
require.NoError(t, err, "should successfully patch finalizers")
|
||||||
|
|
||||||
|
require.NoError(t, helper.Repositories.Resource.Delete(t.Context(), repo, metav1.DeleteOptions{}))
|
||||||
|
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||||
|
_, err := helper.Repositories.Resource.Get(t.Context(), repo, metav1.GetOptions{})
|
||||||
|
assert.True(collect, apierrors.IsNotFound(err), "repository should be deleted")
|
||||||
|
}, time.Second*10, time.Millisecond*50, "repository should be deleted")
|
||||||
|
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||||
|
foundFolders, err := helper.Folders.Resource.List(t.Context(), metav1.ListOptions{})
|
||||||
|
require.NoError(t, err, "can list values")
|
||||||
|
for _, v := range foundFolders.Items {
|
||||||
|
assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeyManagerKind)
|
||||||
|
assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeyManagerIdentity)
|
||||||
|
assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeySourcePath)
|
||||||
|
assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeySourceChecksum)
|
||||||
|
}
|
||||||
|
}, time.Second*20, time.Millisecond*10, "Expected folders to be released")
|
||||||
|
|
||||||
|
libraryElement := map[string]interface{}{
|
||||||
|
"kind": 1,
|
||||||
|
"name": "Library Panel",
|
||||||
|
"folderUid": managedFolderName,
|
||||||
|
"model": map[string]interface{}{
|
||||||
|
"type": "text",
|
||||||
|
"title": "Library Panel",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
libraryElementURL := "/api/library-elements"
|
||||||
|
libraryElementData, code, err := postHelper(t, *helper.K8sTestHelper, libraryElementURL, libraryElement, helper.Org1.Admin)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, code)
|
||||||
|
require.NotNil(t, libraryElementData)
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user