Unistore: Update Folders Endpoint (#97317)

* Add route for update folders via apiserver

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

---------

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>
This commit is contained in:
maicon
2024-12-03 15:33:01 -03:00
committed by GitHub
parent 9d89d8757f
commit 7fd8877439
5 changed files with 336 additions and 34 deletions
+7 -6
View File
@@ -89,7 +89,7 @@ func UnstructuredToLegacyFolder(item unstructured.Unstructured, orgID int64) (*f
if created != nil {
// #TODO Fix this time format. The legacy time format seems to be along the lines of time.Now()
// which includes a part that represents a fraction of a second. Format should be "2024-09-12T15:37:41.09466+02:00"
createdTime = created.Local()
createdTime = (*created).UTC()
}
f := &folder.Folder{
@@ -122,6 +122,10 @@ func UnstructuredToLegacyFolder(item unstructured.Unstructured, orgID int64) (*f
// #TODO figure out about adding version, parents, orgID fields
}
func LegacyFolderToUnstructured(v *folder.Folder, namespacer request.NamespaceMapper) (*v0alpha1.Folder, error) {
return convertToK8sResource(v, namespacer)
}
func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) (*v0alpha1.Folder, error) {
f := &v0alpha1.Folder{
TypeMeta: v0alpha1.FolderResourceInfo.TypeMeta(),
@@ -208,11 +212,8 @@ func getURL(meta utils.GrafanaMetaAccessor, title string) string {
}
func getCreated(meta utils.GrafanaMetaAccessor) (*time.Time, error) {
created, err := meta.GetRepositoryTimestamp()
if err != nil {
return nil, err
}
return created, nil
created := meta.GetCreationTimestamp().Time
return &created, nil
}
func GetParentTitles(fullPath string) ([]string, error) {
+10
View File
@@ -3,6 +3,7 @@ package folders
import (
"context"
"errors"
"fmt"
"slices"
"github.com/prometheus/client_golang/prometheus"
@@ -246,6 +247,15 @@ func (b *FolderAPIBuilder) Validate(ctx context.Context, a admission.Attributes,
obj := a.GetObject()
f, ok := obj.(*v0alpha1.Folder)
if !ok {
return fmt.Errorf("obj is not v0alpha1.Folder")
}
if f.Spec.Title == "" {
return dashboards.ErrFolderTitleEmpty
}
for i := 1; i <= folderValidationRules.maxDepth; i++ {
parent := getParent(obj)
if parent == "" {
+79 -19
View File
@@ -17,7 +17,6 @@ import (
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
@@ -149,6 +148,42 @@ func TestFolderAPIBuilder_getAuthorizerFunc(t *testing.T) {
allow: false,
},
},
{
name: "user with write permissions should be able to update a folder",
input: input{
user: &user.SignedInUser{
UserID: 1,
OrgID: orgID,
Name: "123",
Permissions: map[int64]map[string][]string{
orgID: {dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
},
},
verb: string(utils.VerbUpdate),
},
expect: expect{
eval: "folders:write",
allow: true,
},
},
{
name: "user without write permissions should NOT be able to update a folder",
input: input{
user: &user.SignedInUser{
UserID: 1,
OrgID: orgID,
Name: "123",
Permissions: map[int64]map[string][]string{
orgID: {},
},
},
verb: string(utils.VerbUpdate),
},
expect: expect{
eval: "folders:write",
allow: false,
},
},
}
b := &FolderAPIBuilder{
@@ -177,9 +212,19 @@ func TestFolderAPIBuilder_getAuthorizerFunc(t *testing.T) {
func TestFolderAPIBuilder_Validate(t *testing.T) {
type input struct {
obj *unstructured.Unstructured
name string
obj *v0alpha1.Folder
annotations map[string]string
name string
}
circularObj := &v0alpha1.Folder{
Spec: v0alpha1.Spec{
Title: "foo",
},
}
circularObj.Name = "valid-name"
circularObj.Annotations = map[string]string{"grafana.app/folder": "valid-name"}
tests := []struct {
name string
input input
@@ -189,9 +234,9 @@ func TestFolderAPIBuilder_Validate(t *testing.T) {
{
name: "should return error when name is invalid",
input: input{
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"meta": map[string]interface{}{"name": folderValidationRules.invalidNames[0]},
obj: &v0alpha1.Folder{
Spec: v0alpha1.Spec{
Title: "foo",
},
},
name: folderValidationRules.invalidNames[0],
@@ -201,9 +246,9 @@ func TestFolderAPIBuilder_Validate(t *testing.T) {
{
name: "should return no error if every validation passes",
input: input{
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"meta": map[string]interface{}{"name": "valid-name"},
obj: &v0alpha1.Folder{
Spec: v0alpha1.Spec{
Title: "foo",
},
},
name: "valid-name",
@@ -212,23 +257,33 @@ func TestFolderAPIBuilder_Validate(t *testing.T) {
{
name: "should return error when creating a nested folder higher than max depth",
input: input{
obj: &unstructured.Unstructured{
Object: map[string]any{
"metadata": map[string]any{"name": "valid-name", "annotations": map[string]any{"grafana.app/folder": "valid-name"}},
obj: &v0alpha1.Folder{
Spec: v0alpha1.Spec{
Title: "foo",
},
},
name: "valid-name",
annotations: map[string]string{"grafana.app/folder": "valid-name"},
name: "valid-name",
},
setupFn: func(m *mock.Mock) {
m.On("Get", mock.Anything, "valid-name", mock.Anything).Return(
&unstructured.Unstructured{
Object: map[string]any{
"metadata": map[string]any{"name": "valid-name", "annotations": map[string]any{"grafana.app/folder": "valid-name"}},
},
}, nil)
circularObj,
nil)
},
err: folder.ErrMaximumDepthReached,
},
{
name: "should return error when title is empty",
input: input{
obj: &v0alpha1.Folder{
Spec: v0alpha1.Spec{
Title: "",
},
},
name: "foo",
},
err: dashboards.ErrFolderTitleEmpty,
},
}
s := (grafanarest.Storage)(nil)
@@ -246,6 +301,9 @@ func TestFolderAPIBuilder_Validate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.input.obj.Name = tt.input.name
tt.input.obj.Annotations = tt.input.annotations
if tt.setupFn != nil {
tt.setupFn(m)
}
@@ -264,7 +322,9 @@ func TestFolderAPIBuilder_Validate(t *testing.T) {
&user.SignedInUser{},
), nil)
if tt.err != nil {
if tt.err == nil {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, tt.err)
return
}