Search: expose search on dashboard apiserver (v0alpha1) (#96907)

This commit is contained in:
Ryan McKinley
2024-11-22 23:26:56 +03:00
committed by GitHub
parent 8a006dd4b6
commit 94262fd095
6 changed files with 147 additions and 212 deletions

View File

@ -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
}

View 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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)
})
}