Scopes: Add a /find query endpoint (#87457)

---------

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Todd Treece
2024-05-08 13:35:11 -04:00
committed by GitHub
co-authored by Ryan McKinley
parent 01d28e01d2
commit 6e4d35e1ee
10 changed files with 422 additions and 68 deletions
+99
View File
@@ -0,0 +1,99 @@
package scope
import (
"context"
"fmt"
"net/http"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/rest"
scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1"
)
type findREST struct {
scopeNodeStorage *storage
}
var (
_ rest.Storage = (*findREST)(nil)
_ rest.SingularNameProvider = (*findREST)(nil)
_ rest.Connecter = (*findREST)(nil)
_ rest.Scoper = (*findREST)(nil)
_ rest.StorageMetadata = (*findREST)(nil)
)
func (r *findREST) New() runtime.Object {
// This is added as the "ResponseType" regarless what ProducesObject() says :)
return &scope.TreeResults{}
}
func (r *findREST) Destroy() {}
func (r *findREST) NamespaceScoped() bool {
return true
}
func (r *findREST) GetSingularName() string {
return "TreeResult" // Used for the
}
func (r *findREST) ProducesMIMETypes(verb string) []string {
return []string{"application/json"} // and parquet!
}
func (r *findREST) ProducesObject(verb string) interface{} {
return &scope.TreeResults{}
}
func (r *findREST) ConnectMethods() []string {
return []string{"GET"}
}
func (r *findREST) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, "" // true means you can use the trailing path as a variable
}
func (r *findREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
// See: /pkg/apiserver/builder/helper.go#L34
// The name is set with a rewriter hack
if name != "name" {
return nil, errors.NewNotFound(schema.GroupResource{}, name)
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
parent := req.URL.Query().Get("parent")
results := &scope.TreeResults{}
raw, err := r.scopeNodeStorage.List(ctx, &internalversion.ListOptions{
Limit: 1000,
})
if err != nil {
responder.Error(err)
return
}
all, ok := raw.(*scope.ScopeNodeList)
if !ok {
responder.Error(fmt.Errorf("expected ScopeNodeList"))
return
}
for _, item := range all.Items {
if parent != item.Spec.ParentName && parent != "" {
continue // Someday this will have an index in raw storage on parentName
}
results.Items = append(results.Items, scope.TreeItem{
NodeID: item.Name,
NodeType: item.Spec.NodeType,
Title: item.Spec.Title,
Description: item.Spec.Description,
LinkType: item.Spec.LinkType,
LinkID: item.Spec.LinkID,
})
}
responder.Object(200, results)
}), nil
}
+49
View File
@@ -12,6 +12,7 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1"
"github.com/grafana/grafana/pkg/apiserver/builder"
@@ -140,6 +141,11 @@ func (b *ScopeAPIBuilder) GetAPIGroupInfo(
}
storage[scopeNodeResourceInfo.StoragePath()] = scopeNodeStorage
// Adds a rest.Connector
// NOTE! the server has a hardcoded rewrite filter that fills in a name
// so the standard k8s plumbing continues to work
storage["find"] = &findREST{scopeNodeStorage: scopeNodeStorage}
apiGroupInfo.VersionedResourcesStorageMap[scope.VERSION] = storage
return &apiGroupInfo, nil
}
@@ -152,3 +158,46 @@ func (b *ScopeAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
func (b *ScopeAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
return nil
}
func (b *ScopeAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
// The plugin description
oas.Info.Description = "Grafana scopes"
// The root api URL
root := "/apis/" + b.GetGroupVersion().String() + "/"
// Add query parameters to the rest.Connector
sub := oas.Paths.Paths[root+"namespaces/{namespace}/find/{name}"]
if sub != nil && sub.Get != nil {
sub.Parameters = []*spec3.Parameter{
{
ParameterProps: spec3.ParameterProps{
Name: "namespace",
In: "path",
Description: "object name and auth scope, such as for teams and projects",
Example: "default",
Required: true,
},
},
}
sub.Get.Description = "Navigate the scopes tree"
sub.Get.Parameters = []*spec3.Parameter{
{
ParameterProps: spec3.ParameterProps{
Name: "parent",
In: "query",
Description: "The parent scope node",
},
},
}
delete(oas.Paths.Paths, root+"namespaces/{namespace}/find/{name}")
oas.Paths.Paths[root+"namespaces/{namespace}/find"] = sub
}
// The root API discovery list
sub = oas.Paths.Paths[root]
if sub != nil && sub.Get != nil {
sub.Get.Tags = []string{"API Discovery"} // sorts first in the list
}
return oas, nil
}
+3 -3
View File
@@ -10,12 +10,12 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
apistore "k8s.io/apiserver/pkg/storage"
scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/utils"
apistore "k8s.io/apiserver/pkg/storage"
)
var _ grafanarest.Storage = (*storage)(nil)
@@ -176,8 +176,8 @@ func SelectableScopeDashboardBindingFields(obj *scope.ScopeDashboardBinding) fie
func SelectableScopeNodeFields(obj *scope.ScopeNode) fields.Set {
parentName := ""
if obj.Spec.ParentName != nil {
parentName = *obj.Spec.ParentName
if obj != nil {
parentName = obj.Spec.ParentName
}
return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{