mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 01:56:56 +08:00
Search: expose search on dashboard apiserver (v0alpha1) (#96907)
This commit is contained in:
@ -16,7 +16,8 @@ import (
|
||||
func GetAuthorizer(dashboardService dashboards.DashboardService, l log.Logger) authorizer.Authorizer {
|
||||
return authorizer.AuthorizerFunc(
|
||||
func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
if !attr.IsResourceRequest() {
|
||||
// Use the standard authorizer
|
||||
if !attr.IsResourceRequest() || attr.GetResource() == "search" {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
|
122
pkg/registry/apis/dashboard/search.go
Normal file
122
pkg/registry/apis/dashboard/search.go
Normal file
@ -0,0 +1,122 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
// The DTO returns everything the UI needs in a single request
|
||||
type SearchConnector struct {
|
||||
newFunc func() runtime.Object
|
||||
client resource.ResourceIndexClient
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewSearchConnector(
|
||||
client resource.ResourceIndexClient,
|
||||
newFunc func() runtime.Object,
|
||||
) (rest.Storage, error) {
|
||||
v := &SearchConnector{
|
||||
client: client,
|
||||
newFunc: newFunc,
|
||||
log: log.New("grafana-apiserver.dashboards.search"),
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ rest.Connecter = (*SearchConnector)(nil)
|
||||
_ rest.StorageMetadata = (*SearchConnector)(nil)
|
||||
_ rest.Scoper = (*SearchConnector)(nil)
|
||||
_ rest.SingularNameProvider = (*SearchConnector)(nil)
|
||||
)
|
||||
|
||||
func (s *SearchConnector) New() runtime.Object {
|
||||
return s.newFunc()
|
||||
}
|
||||
|
||||
func (s *SearchConnector) Destroy() {
|
||||
}
|
||||
|
||||
func (s *SearchConnector) NamespaceScoped() bool {
|
||||
return true // namespace == org
|
||||
}
|
||||
|
||||
func (s *SearchConnector) GetSingularName() string {
|
||||
return "Search"
|
||||
}
|
||||
|
||||
func (s *SearchConnector) ConnectMethods() []string {
|
||||
return []string{"GET"}
|
||||
}
|
||||
|
||||
func (s *SearchConnector) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, false, ""
|
||||
}
|
||||
|
||||
func (s *SearchConnector) ProducesMIMETypes(verb string) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SearchConnector) ProducesObject(verb string) interface{} {
|
||||
return s.newFunc()
|
||||
}
|
||||
|
||||
func (s *SearchConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
queryParams, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// get limit and offset from query params
|
||||
limit := 0
|
||||
offset := 0
|
||||
if queryParams.Has("limit") {
|
||||
limit, _ = strconv.Atoi(queryParams.Get("limit"))
|
||||
}
|
||||
if queryParams.Has("offset") {
|
||||
offset, _ = strconv.Atoi(queryParams.Get("offset"))
|
||||
}
|
||||
|
||||
searchRequest := &resource.SearchRequest{
|
||||
Tenant: user.GetNamespace(), //<< not necessary it is in the namespace (and user context)
|
||||
Kind: strings.Split(queryParams.Get("kind"), ","),
|
||||
QueryType: queryParams.Get("queryType"),
|
||||
Query: queryParams.Get("query"),
|
||||
Limit: int64(limit),
|
||||
Offset: int64(offset),
|
||||
}
|
||||
|
||||
// TODO... actually query
|
||||
result, err := s.client.Search(r.Context(), searchRequest)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
jj, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(jj)
|
||||
}), nil
|
||||
}
|
@ -177,6 +177,14 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
|
||||
return err
|
||||
}
|
||||
|
||||
// Requires hack in to resolve with no name:
|
||||
// pkg/services/apiserver/builder/helper.go#L58
|
||||
storage["search"], err = dashboard.NewSearchConnector(b.unified,
|
||||
func() runtime.Object { return &dashboardv0alpha1.DashboardWithAccessInfo{} }) // TODO... replace with a real model
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Expose read only library panels
|
||||
storage[dashboardv0alpha1.LibraryPanelResourceInfo.StoragePath()] = &dashboard.LibraryPanelStore{
|
||||
Access: b.legacy.Access,
|
||||
@ -202,8 +210,13 @@ func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op
|
||||
delete(oas.Paths.Paths, root+dashboardv0alpha1.DashboardResourceInfo.GroupResource().Resource)
|
||||
delete(oas.Paths.Paths, root+"watch/"+dashboardv0alpha1.DashboardResourceInfo.GroupResource().Resource)
|
||||
|
||||
// Resolve the empty name
|
||||
sub := oas.Paths.Paths[root+"search/{name}"]
|
||||
oas.Paths.Paths[root+"search"] = sub
|
||||
delete(oas.Paths.Paths, root+"search/{name}")
|
||||
|
||||
// The root API discovery list
|
||||
sub := oas.Paths.Paths[root]
|
||||
sub = oas.Paths.Paths[root]
|
||||
if sub != nil && sub.Get != nil {
|
||||
sub.Get.Tags = []string{"API Discovery"} // sorts first in the list
|
||||
}
|
||||
|
@ -54,6 +54,12 @@ var PathRewriters = []filters.PathRewriter{
|
||||
return matches[1] + "/name" // connector requires a name
|
||||
},
|
||||
},
|
||||
{
|
||||
Pattern: regexp.MustCompile(`(/apis/dashboard.grafana.app/v0alpha1/namespaces/.*/search$)`),
|
||||
ReplaceFunc: func(matches []string) string {
|
||||
return matches[1] + "/name" // connector requires a name
|
||||
},
|
||||
},
|
||||
{
|
||||
Pattern: regexp.MustCompile(`(/apis/.*/v0alpha1/namespaces/.*/queryconvert$)`),
|
||||
ReplaceFunc: func(matches []string) string {
|
||||
|
@ -927,6 +927,9 @@ func (s *server) Search(ctx context.Context, req *SearchRequest) (*SearchRespons
|
||||
if err := s.Init(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.index == nil {
|
||||
return nil, fmt.Errorf("search index not configured")
|
||||
}
|
||||
return s.index.Search(ctx, req)
|
||||
}
|
||||
|
||||
|
@ -196,214 +196,4 @@ func TestIntegrationDashboardsApp(t *testing.T) {
|
||||
})
|
||||
runDashboardTest(t, helper)
|
||||
})
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: false, // required for experimental APIs
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesDashboardsAPI, // Required to start the example service
|
||||
},
|
||||
})
|
||||
|
||||
_, err := helper.NewDiscoveryClient().ServerResourcesForGroupVersion("dashboard.grafana.app/v0alpha1")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("Check discovery client", func(t *testing.T) {
|
||||
disco := helper.GetGroupVersionInfoJSON("dashboard.grafana.app")
|
||||
|
||||
require.JSONEq(t, `[
|
||||
{
|
||||
"freshness": "Current",
|
||||
"resources": [
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "Dashboard",
|
||||
"version": ""
|
||||
},
|
||||
"scope": "Namespaced",
|
||||
"singularResource": "dashboard",
|
||||
"subresources": [
|
||||
{
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "DashboardWithAccessInfo",
|
||||
"version": ""
|
||||
},
|
||||
"subresource": "dto",
|
||||
"verbs": [
|
||||
"get"
|
||||
]
|
||||
},
|
||||
{
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "PartialObjectMetadataList",
|
||||
"version": ""
|
||||
},
|
||||
"subresource": "history",
|
||||
"verbs": [
|
||||
"get"
|
||||
]
|
||||
}
|
||||
],
|
||||
"verbs": [
|
||||
"create",
|
||||
"delete",
|
||||
"deletecollection",
|
||||
"get",
|
||||
"list",
|
||||
"patch",
|
||||
"update",
|
||||
"watch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "librarypanels",
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "LibraryPanel",
|
||||
"version": ""
|
||||
},
|
||||
"scope": "Namespaced",
|
||||
"singularResource": "librarypanel",
|
||||
"verbs": [
|
||||
"get",
|
||||
"list"
|
||||
]
|
||||
}
|
||||
],
|
||||
"version": "v2alpha1"
|
||||
},
|
||||
{
|
||||
"freshness": "Current",
|
||||
"resources": [
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "Dashboard",
|
||||
"version": ""
|
||||
},
|
||||
"scope": "Namespaced",
|
||||
"singularResource": "dashboard",
|
||||
"subresources": [
|
||||
{
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "DashboardWithAccessInfo",
|
||||
"version": ""
|
||||
},
|
||||
"subresource": "dto",
|
||||
"verbs": [
|
||||
"get"
|
||||
]
|
||||
},
|
||||
{
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "PartialObjectMetadataList",
|
||||
"version": ""
|
||||
},
|
||||
"subresource": "history",
|
||||
"verbs": [
|
||||
"get"
|
||||
]
|
||||
}
|
||||
],
|
||||
"verbs": [
|
||||
"create",
|
||||
"delete",
|
||||
"deletecollection",
|
||||
"get",
|
||||
"list",
|
||||
"patch",
|
||||
"update",
|
||||
"watch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "librarypanels",
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "LibraryPanel",
|
||||
"version": ""
|
||||
},
|
||||
"scope": "Namespaced",
|
||||
"singularResource": "librarypanel",
|
||||
"verbs": [
|
||||
"get",
|
||||
"list"
|
||||
]
|
||||
}
|
||||
],
|
||||
"version": "v1alpha1"
|
||||
},
|
||||
{
|
||||
"freshness": "Current",
|
||||
"resources": [
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "Dashboard",
|
||||
"version": ""
|
||||
},
|
||||
"scope": "Namespaced",
|
||||
"singularResource": "dashboard",
|
||||
"subresources": [
|
||||
{
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "DashboardWithAccessInfo",
|
||||
"version": ""
|
||||
},
|
||||
"subresource": "dto",
|
||||
"verbs": [
|
||||
"get"
|
||||
]
|
||||
},
|
||||
{
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "PartialObjectMetadataList",
|
||||
"version": ""
|
||||
},
|
||||
"subresource": "history",
|
||||
"verbs": [
|
||||
"get"
|
||||
]
|
||||
}
|
||||
],
|
||||
"verbs": [
|
||||
"create",
|
||||
"delete",
|
||||
"deletecollection",
|
||||
"get",
|
||||
"list",
|
||||
"patch",
|
||||
"update",
|
||||
"watch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "librarypanels",
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "LibraryPanel",
|
||||
"version": ""
|
||||
},
|
||||
"scope": "Namespaced",
|
||||
"singularResource": "librarypanel",
|
||||
"verbs": [
|
||||
"get",
|
||||
"list"
|
||||
]
|
||||
}
|
||||
],
|
||||
"version": "v0alpha1"
|
||||
}
|
||||
]`, disco)
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user