mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s: Implement partial folders api with k8s client (#93089)
* Add kubernetes folder feature toggle * Add kubernetes routes for getting and creating a folder * Add documentation for interacting with k8s folders
This commit is contained in:
@@ -156,6 +156,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) |
|
||||
| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint |
|
||||
| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards |
|
||||
| `kubernetesFolders` | Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s |
|
||||
| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) |
|
||||
| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query |
|
||||
| `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service |
|
||||
|
||||
@@ -115,6 +115,7 @@ export interface FeatureToggles {
|
||||
kubernetesPlaylists?: boolean;
|
||||
kubernetesSnapshots?: boolean;
|
||||
kubernetesDashboards?: boolean;
|
||||
kubernetesFolders?: boolean;
|
||||
datasourceQueryTypes?: boolean;
|
||||
queryService?: boolean;
|
||||
queryServiceRewrite?: boolean;
|
||||
|
||||
@@ -443,7 +443,16 @@ 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.Post("/", handler.createFolder)
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Get("/", handler.getFolder)
|
||||
})
|
||||
} 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))
|
||||
@@ -462,6 +471,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
folderPermissionRoute.Post("/", authorize(ac.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Dashboard
|
||||
|
||||
@@ -6,13 +6,22 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"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/apimachinery/identity"
|
||||
folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||
@@ -23,6 +32,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/util/errhttp"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@@ -619,3 +629,77 @@ type GetFolderDescendantCountsResponse struct {
|
||||
// in: body
|
||||
Body folder.DescendantCounts `json:"body"`
|
||||
}
|
||||
|
||||
type folderK8sHandler struct {
|
||||
namespacer request.NamespaceMapper
|
||||
gvr schema.GroupVersionResource
|
||||
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
// Folder k8s wrapper functions
|
||||
//-----------------------------------------------------------------------------------------
|
||||
|
||||
func newFolderK8sHandler(hs *HTTPServer) *folderK8sHandler {
|
||||
return &folderK8sHandler{
|
||||
gvr: folderalpha1.FolderResourceInfo.GroupVersionResource(),
|
||||
namespacer: request.GetNamespaceMapper(hs.Cfg),
|
||||
clientConfigProvider: hs.clientConfigProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) createFolder(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
cmd := folder.CreateFolderCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
c.JsonApiErr(http.StatusBadRequest, "bad request data", err)
|
||||
return
|
||||
}
|
||||
obj := internalfolders.LegacyCreateCommandToUnstructured(cmd)
|
||||
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))
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
uid := web.Params(c.Req)[":uid"]
|
||||
out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, internalfolders.UnstructuredToLegacyFolderDTO(*out))
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
// Utility functions
|
||||
//-----------------------------------------------------------------------------------------
|
||||
|
||||
func (fk8s *folderK8sHandler) getClient(c *contextmodel.ReqContext) (dynamic.ResourceInterface, bool) {
|
||||
dyn, err := dynamic.NewForConfig(fk8s.clientConfigProvider.GetDirectRestConfig(c))
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "client", err)
|
||||
return nil, false
|
||||
}
|
||||
return dyn.Resource(fk8s.gvr).Namespace(fk8s.namespacer(c.OrgID)), true
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) writeError(c *contextmodel.ReqContext, err error) {
|
||||
//nolint:errorlint
|
||||
statusError, ok := err.(*k8sErrors.StatusError)
|
||||
if ok {
|
||||
c.JsonApiErr(int(statusError.Status().Code), statusError.Status().Message, err)
|
||||
return
|
||||
}
|
||||
errhttp.Write(c.Req.Context(), err, c.Resp)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"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/services/apiserver/endpoints/request"
|
||||
@@ -12,6 +14,29 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
)
|
||||
|
||||
func LegacyCreateCommandToUnstructured(cmd folder.CreateFolderCommand) unstructured.Unstructured {
|
||||
// #TODO add other fields
|
||||
obj := unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"title": cmd.Title,
|
||||
},
|
||||
},
|
||||
}
|
||||
obj.SetName(cmd.UID)
|
||||
return obj
|
||||
}
|
||||
|
||||
func UnstructuredToLegacyFolderDTO(item unstructured.Unstructured) *dtos.Folder {
|
||||
spec := item.Object["spec"].(map[string]any)
|
||||
dto := &dtos.Folder{
|
||||
UID: item.GetName(),
|
||||
Title: spec["title"].(string),
|
||||
// #TODO add other fields
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) *v0alpha1.Folder {
|
||||
f := &v0alpha1.Folder{
|
||||
TypeMeta: v0alpha1.FolderResourceInfo.TypeMeta(),
|
||||
|
||||
@@ -735,6 +735,12 @@ var (
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "kubernetesFolders",
|
||||
Description: "Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaSearchAndStorageSquad,
|
||||
},
|
||||
{
|
||||
Name: "datasourceQueryTypes",
|
||||
Description: "Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)",
|
||||
|
||||
@@ -96,6 +96,7 @@ transformationsVariableSupport,GA,@grafana/dataviz-squad,false,false,true
|
||||
kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false
|
||||
kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true
|
||||
kubernetesFolders,experimental,@grafana/search-and-storage,false,false,false
|
||||
datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
queryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
|
||||
|
@@ -395,6 +395,10 @@ const (
|
||||
// Use the kubernetes API in the frontend for dashboards
|
||||
FlagKubernetesDashboards = "kubernetesDashboards"
|
||||
|
||||
// FlagKubernetesFolders
|
||||
// Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s
|
||||
FlagKubernetesFolders = "kubernetesFolders"
|
||||
|
||||
// FlagDatasourceQueryTypes
|
||||
// Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)
|
||||
FlagDatasourceQueryTypes = "datasourceQueryTypes"
|
||||
|
||||
@@ -1519,6 +1519,21 @@
|
||||
"hideFromAdminPage": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesFolders",
|
||||
"resourceVersion": "1725863636605",
|
||||
"creationTimestamp": "2024-09-09T06:29:38Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2024-09-09 06:33:56.605329 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/search-and-storage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesPlaylists",
|
||||
|
||||
@@ -13,7 +13,7 @@ There are 2 main tables, the `resource` table stores a "current" view of the obj
|
||||
|
||||
## Running Unified Storage
|
||||
|
||||
### Baseline configuration
|
||||
### Playlists: baseline configuration
|
||||
|
||||
The minimum config settings required are:
|
||||
|
||||
@@ -34,6 +34,32 @@ kubernetesPlaylists = true
|
||||
storage_type = unified
|
||||
```
|
||||
|
||||
### Folders: baseline configuration
|
||||
|
||||
NOTE: allowing folders to be backed by Unified Storage is under development and so are these instructions.
|
||||
|
||||
The minimum config settings required are:
|
||||
|
||||
```ini
|
||||
; need to specify target here for override to work later
|
||||
target = all
|
||||
|
||||
[server]
|
||||
; https is required for kubectl
|
||||
protocol = https
|
||||
|
||||
[feature_toggles]
|
||||
; store folders in k8s
|
||||
kubernetesFolders = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
|
||||
[grafana-apiserver]
|
||||
; use unified storage for k8s apiserver
|
||||
storage_type = unified
|
||||
```
|
||||
|
||||
### Setting up a kubeconfig
|
||||
|
||||
With this configuration, you can run everything in-process. Run the Grafana backend with:
|
||||
|
||||
```sh
|
||||
@@ -75,6 +101,8 @@ Where `<username>` and `<password>` are credentials for basic auth against Grafa
|
||||
password: admin
|
||||
```
|
||||
|
||||
### Playlists: interacting with the k8s API
|
||||
|
||||
In this mode, you can interact with the k8s api. Make sure you are in the directory where you created `grafana.kubeconfig`. Then run:
|
||||
```sh
|
||||
kubectl --kubeconfig=./grafana.kubeconfig get playlist
|
||||
@@ -132,6 +160,32 @@ kubectl --kubeconfig=./grafana.kubeconfig patch playlist <NAME> --patch-file pla
|
||||
|
||||
In the example, `<NAME>` would be `u394j4d3-s63j-2d74-g8hf-958773jtybf2`.
|
||||
|
||||
### Folders: interacting with the k8s API
|
||||
|
||||
Make sure you are in the directory where you created `grafana.kubeconfig`. Then run:
|
||||
```sh
|
||||
kubectl --kubeconfig=./grafana.kubeconfig get folder
|
||||
```
|
||||
|
||||
If this is your first time running the command, a successful response would be:
|
||||
```sh
|
||||
No resources found in default namespace.
|
||||
```
|
||||
|
||||
To create a folder, create a file `folder-generate.yaml`:
|
||||
```yaml
|
||||
apiVersion: folder.grafana.app/v0alpha1
|
||||
kind: Folder
|
||||
metadata:
|
||||
generateName: x # anything is ok here... except yes or true -- they become boolean!
|
||||
spec:
|
||||
title: Example folder
|
||||
```
|
||||
then run:
|
||||
```sh
|
||||
kubectl --kubeconfig=./grafana.kubeconfig create -f folder-generate.yaml
|
||||
```
|
||||
|
||||
### Use a separate database
|
||||
|
||||
By default Unified Storage uses the Grafana database. To run against a separate database, update `custom.ini` by adding the following section to it:
|
||||
|
||||
Reference in New Issue
Block a user