K8s/Folders: Convert additional fields when creating k8s resources (#93395)

* Add separate folder registration function
* Convert to k8s resource directly after legacy create
* Use create command when creating folders
* Set additional fields when converting to k8s resource
* Add created/updated timestamps during conversion
* Refactor UnstructuredToLegacyFolderDTO
* Return errors when doing k8s conversions
This commit is contained in:
Arati R. 2024-09-25 08:56:15 +02:00 committed by GitHub
parent 8c5dfa33d4
commit 2c26053be8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 213 additions and 63 deletions

View File

@ -443,39 +443,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Any("/datasources/uid/:uid/health", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), routing.Wrap(hs.CheckDatasourceHealthWithUID))
// Folders
// #TODO kubernetes folders: move this to its own function, add back auth part, add other routes
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) {
// Use k8s client to implement legacy API
handler := newFolderK8sHandler(hs)
folderRoute.Get("/", handler.searchFolders)
folderRoute.Post("/", handler.createFolder)
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Get("/", handler.getFolder)
folderUidRoute.Delete("/", handler.deleteFolder)
folderUidRoute.Put("/:uid", handler.updateFolder)
})
} else {
idScope := dashboards.ScopeFoldersProvider.GetResourceScope(ac.Parameter(":id"))
uidScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.Parameter(":uid"))
folderRoute.Get("/", authorize(ac.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
folderRoute.Get("/id/:id", authorize(ac.EvalPermission(dashboards.ActionFoldersRead, idScope)), routing.Wrap(hs.GetFolderByID))
folderRoute.Post("/", authorize(ac.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Get("/", authorize(ac.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID))
folderUidRoute.Put("/", authorize(ac.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
folderUidRoute.Post("/move", authorize(ac.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder))
folderUidRoute.Delete("/", authorize(ac.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder))
folderUidRoute.Get("/counts", authorize(ac.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts))
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
folderPermissionRoute.Get("/", authorize(ac.EvalPermission(dashboards.ActionFoldersPermissionsRead, uidScope)), routing.Wrap(hs.GetFolderPermissionList))
folderPermissionRoute.Post("/", authorize(ac.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
})
})
}
})
hs.registerFolderAPI(apiRoute, authorize)
// Dashboard
apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) {

View File

@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/api/apierrors"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/infra/metrics"
@ -39,6 +40,42 @@ import (
const REDACTED = "redacted"
func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authorize func(accesscontrol.Evaluator) web.Handler) {
// #TODO add back auth part
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) {
// Use k8s client to implement legacy API
handler := newFolderK8sHandler(hs)
folderRoute.Get("/", handler.searchFolders)
folderRoute.Post("/", handler.createFolder)
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Get("/", handler.getFolder)
folderUidRoute.Delete("/", handler.deleteFolder)
folderUidRoute.Put("/:uid", handler.updateFolder)
})
} else {
idScope := dashboards.ScopeFoldersProvider.GetResourceScope(accesscontrol.Parameter(":id"))
uidScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.Parameter(":uid"))
folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
folderRoute.Get("/id/:id", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, idScope)), routing.Wrap(hs.GetFolderByID))
folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID))
folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder))
folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder))
folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts))
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
folderPermissionRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, uidScope)), routing.Wrap(hs.GetFolderPermissionList))
folderPermissionRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
})
})
}
})
}
// swagger:route GET /folders folders getFolders
//
// Get all folders.
@ -680,18 +717,28 @@ func (fk8s *folderK8sHandler) createFolder(c *contextmodel.ReqContext) {
if !ok {
return // error is already sent
}
cmd := folder.UpdateFolderCommand{}
cmd := folder.CreateFolderCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
c.JsonApiErr(http.StatusBadRequest, "bad request data", err)
return
}
obj := internalfolders.LegacyUpdateCommandToUnstructured(cmd)
obj, err := internalfolders.LegacyCreateCommandToUnstructured(cmd)
if err != nil {
fk8s.writeError(c, err)
return
}
out, err := client.Create(c.Req.Context(), &obj, v1.CreateOptions{})
if err != nil {
fk8s.writeError(c, err)
return
}
c.JSON(http.StatusOK, internalfolders.UnstructuredToLegacyFolderDTO(*out))
f, err := internalfolders.UnstructuredToLegacyFolderDTO(*out)
if err != nil {
fk8s.writeError(c, err)
return
}
c.JSON(http.StatusOK, f)
}
func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) {
@ -705,7 +752,14 @@ func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) {
fk8s.writeError(c, err)
return
}
c.JSON(http.StatusOK, internalfolders.UnstructuredToLegacyFolderDTO(*out))
f, err := internalfolders.UnstructuredToLegacyFolderDTO(*out)
if err != nil {
fk8s.writeError(c, err)
return
}
c.JSON(http.StatusOK, f)
}
func (fk8s *folderK8sHandler) deleteFolder(c *contextmodel.ReqContext) {
@ -740,7 +794,14 @@ func (fk8s *folderK8sHandler) updateFolder(c *contextmodel.ReqContext) {
fk8s.writeError(c, err)
return
}
c.JSON(http.StatusOK, internalfolders.UnstructuredToLegacyFolderDTO(*out))
f, err := internalfolders.UnstructuredToLegacyFolderDTO(*out)
if err != nil {
fk8s.writeError(c, err)
return
}
c.JSON(http.StatusOK, f)
}
//-----------------------------------------------------------------------------------------

View File

@ -2,6 +2,8 @@ package folders
import (
"fmt"
"strconv"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -9,11 +11,32 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder"
)
func LegacyCreateCommandToUnstructured(cmd folder.CreateFolderCommand) (unstructured.Unstructured, error) {
obj := unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]interface{}{
"title": cmd.Title,
"description": cmd.Description,
},
},
}
// #TODO: let's see if we need to set the json field to "-"
obj.SetName(cmd.UID)
if err := setParentUID(&obj, cmd.ParentUID); err != nil {
return unstructured.Unstructured{}, err
}
return obj, nil
}
func LegacyUpdateCommandToUnstructured(cmd folder.UpdateFolderCommand) unstructured.Unstructured {
// #TODO add other fields
obj := unstructured.Unstructured{
@ -36,17 +59,55 @@ func UnstructuredToLegacyFolder(item unstructured.Unstructured) *folder.Folder {
}
}
func UnstructuredToLegacyFolderDTO(item unstructured.Unstructured) *dtos.Folder {
func UnstructuredToLegacyFolderDTO(item unstructured.Unstructured) (*dtos.Folder, error) {
spec := item.Object["spec"].(map[string]any)
dto := &dtos.Folder{
UID: item.GetName(),
Title: spec["title"].(string),
// #TODO add other fields
uid := item.GetName()
title := spec["title"].(string)
meta, err := utils.MetaAccessor(&item)
if err != nil {
return nil, err
}
return dto
id, err := getLegacyID(meta)
if err != nil {
return nil, err
}
created, err := getCreated(meta)
if err != nil {
return nil, err
}
dto := &dtos.Folder{
UID: uid,
Title: title,
ID: id,
ParentUID: meta.GetFolder(),
// #TODO add back CreatedBy, UpdatedBy once we figure out how to access userService
// to translate user ID into user login. meta.GetCreatedBy() only stores user ID
// Could convert meta.GetCreatedBy() return value to a struct--id and name
// CreatedBy: meta.GetCreatedBy(),
// UpdatedBy: meta.GetCreatedBy(),
URL: getURL(meta, title),
// #TODO get Created in format "2024-09-12T15:37:41.09466+02:00"
Created: *created,
// #TODO figure out whether we want to set "updated" and "updated by". Could replace with
// meta.GetUpdatedTimestamp() but it currently gets overwritten in prepareObjectForStorage().
Updated: *created,
// #TODO figure out how to set these properly
CanSave: true,
CanEdit: true,
CanAdmin: true,
CanDelete: true,
HasACL: false,
// #TODO figure out about adding version, parents, orgID fields
}
return dto, nil
}
func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) *v0alpha1.Folder {
func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) (*v0alpha1.Folder, error) {
f := &v0alpha1.Folder{
TypeMeta: v0alpha1.FolderResourceInfo.TypeMeta(),
ObjectMeta: metav1.ObjectMeta{
@ -62,24 +123,67 @@ func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper)
}
meta, err := utils.MetaAccessor(f)
if err == nil {
meta.SetUpdatedTimestamp(&v.Updated)
if v.ID > 0 { // nolint:staticcheck
meta.SetOriginInfo(&utils.ResourceOriginInfo{
Name: "SQL",
Path: fmt.Sprintf("%d", v.ID), // nolint:staticcheck
})
}
if v.CreatedBy > 0 {
meta.SetCreatedBy(fmt.Sprintf("user:%d", v.CreatedBy))
}
if v.UpdatedBy > 0 {
meta.SetUpdatedBy(fmt.Sprintf("user:%d", v.UpdatedBy))
}
if err != nil {
return nil, err
}
meta.SetUpdatedTimestamp(&v.Updated)
if v.ID > 0 { // nolint:staticcheck
meta.SetOriginInfo(&utils.ResourceOriginInfo{
Name: "SQL",
Path: fmt.Sprintf("%d", v.ID), // nolint:staticcheck
Timestamp: &v.Created,
})
}
if v.CreatedBy > 0 {
meta.SetCreatedBy(fmt.Sprintf("user:%d", v.CreatedBy))
}
if v.UpdatedBy > 0 {
meta.SetUpdatedBy(fmt.Sprintf("user:%d", v.UpdatedBy))
}
if v.ParentUID != "" {
meta.SetFolder(v.ParentUID)
}
f.UID = gapiutil.CalculateClusterWideUID(f)
return f
return f, nil
}
func setParentUID(u *unstructured.Unstructured, parentUid string) error {
meta, err := utils.MetaAccessor(u)
if err != nil {
return err
}
meta.SetFolder(parentUid)
return nil
}
func getLegacyID(meta utils.GrafanaMetaAccessor) (int64, error) {
var i int64
info, err := meta.GetOriginInfo()
if err != nil {
return i, err
}
if info != nil && info.Name == "SQL" {
i, err = strconv.ParseInt(info.Path, 10, 64)
if err != nil {
return i, err
}
}
return i, nil
}
func getURL(meta utils.GrafanaMetaAccessor, title string) string {
slug := slugify.Slugify(title)
uid := meta.GetName()
return dashboards.GetFolderURL(uid, slug)
}
func getCreated(meta utils.GrafanaMetaAccessor) (*time.Time, error) {
created, err := meta.GetOriginTimestamp()
if err != nil {
return nil, err
}
return created, nil
}

View File

@ -101,7 +101,11 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
list := &v0alpha1.FolderList{}
for _, v := range hits {
list.Items = append(list.Items, *convertToK8sResource(v, s.namespacer))
r, err := convertToK8sResource(v, s.namespacer)
if err != nil {
return nil, err
}
list.Items = append(list.Items, *r)
}
if len(list.Items) >= int(paging.limit) {
list.Continue = paging.GetNextPageToken()
@ -132,7 +136,12 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge
return nil, err
}
return convertToK8sResource(dto, s.namespacer), nil
r, err := convertToK8sResource(dto, s.namespacer)
if err != nil {
return nil, err
}
return r, nil
}
func (s *legacyStorage) Create(ctx context.Context,
@ -178,7 +187,15 @@ func (s *legacyStorage) Create(ctx context.Context,
if err != nil {
return nil, err
}
return s.Get(ctx, out.UID, nil)
// #TODO can we directly convert instead of doing a Get? the result of the Create
// has more data than the one of Get so there is more we can include in the k8s resource
// this way
r, err := convertToK8sResource(out, s.namespacer)
if err != nil {
return nil, err
}
return r, nil
}
func (s *legacyStorage) Update(ctx context.Context,