mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
8c5dfa33d4
commit
2c26053be8
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user