scopes: moves scopes to enterprise (#100746)

Signed-off-by: bergquist <carl.bergquist@gmail.com>
This commit is contained in:
Carl Bergquist
2025-02-21 08:51:34 +01:00
committed by GitHub
parent 0290da6aaa
commit 79c0e5e3ec
29 changed files with 289 additions and 2667 deletions
-2
View File
@@ -13,7 +13,6 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/iam"
"github.com/grafana/grafana/pkg/registry/apis/provisioning"
"github.com/grafana/grafana/pkg/registry/apis/query"
"github.com/grafana/grafana/pkg/registry/apis/scope"
"github.com/grafana/grafana/pkg/registry/apis/userstorage"
)
@@ -31,7 +30,6 @@ func ProvideRegistryServiceSink(
_ *datasource.DataSourceAPIBuilder,
_ *folders.FolderAPIBuilder,
_ *iam.IdentityAccessManagementAPIBuilder,
_ *scope.ScopeAPIBuilder,
_ *query.QueryAPIBuilder,
_ *notifications.NotificationsAPIBuilder,
_ *userstorage.UserStorageAPIBuilder,
-113
View File
@@ -1,113 +0,0 @@
package scope
import (
"context"
"fmt"
"net/http"
"strings"
"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"
"github.com/grafana/grafana/pkg/infra/log"
)
var logger = log.New("find-scopenode")
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.FindScopeNodeChildrenResults{}
}
func (r *findREST) Destroy() {}
func (r *findREST) NamespaceScoped() bool {
return true
}
func (r *findREST) GetSingularName() string {
return "FindScopeNodeChildrenResults" // 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.FindScopeNodeChildrenResults{}
}
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/services/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")
query := req.URL.Query().Get("query")
results := &scope.FindScopeNodeChildrenResults{}
raw, err := r.scopeNodeStorage.List(ctx, &internalversion.ListOptions{
Limit: 10000,
})
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 {
filterAndAppendItem(item, parent, query, results)
}
logger.FromContext(req.Context()).Debug("find scopenode", "raw", len(all.Items), "filtered", len(results.Items))
responder.Object(200, results)
}), nil
}
func filterAndAppendItem(item scope.ScopeNode, parent string, query string, results *scope.FindScopeNodeChildrenResults) {
if parent != item.Spec.ParentName {
return // Someday this will have an index in raw storage on parentName
}
// skip if query is passed and title doesn't contain the query.
if query != "" && !strings.Contains(item.Spec.Title, query) {
return
}
results.Items = append(results.Items, item)
}
@@ -1,106 +0,0 @@
package scope
import (
"context"
"fmt"
"net/http"
"slices"
"strings"
scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1"
"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"
)
type findScopeDashboardsREST struct {
scopeDashboardStorage *storage
}
var (
_ rest.Storage = (*findScopeDashboardsREST)(nil)
_ rest.SingularNameProvider = (*findScopeDashboardsREST)(nil)
_ rest.Connecter = (*findScopeDashboardsREST)(nil)
_ rest.Scoper = (*findScopeDashboardsREST)(nil)
_ rest.StorageMetadata = (*findScopeDashboardsREST)(nil)
)
func (f *findScopeDashboardsREST) New() runtime.Object {
return &scope.FindScopeDashboardBindingsResults{}
}
func (f *findScopeDashboardsREST) Destroy() {}
func (f *findScopeDashboardsREST) NamespaceScoped() bool {
return true
}
func (f *findScopeDashboardsREST) GetSingularName() string {
return "FindScopeDashboardsResult" // not sure if this is actually used, but it is required to exist
}
func (f *findScopeDashboardsREST) ProducesMIMETypes(verb string) []string {
return []string{"application/json"} // and parquet!
}
func (f *findScopeDashboardsREST) ProducesObject(verb string) interface{} {
return &scope.FindScopeDashboardBindingsResults{}
}
func (f *findScopeDashboardsREST) ConnectMethods() []string {
return []string{"GET"}
}
func (f *findScopeDashboardsREST) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, "" // true means you can use the trailing path as a variable
}
func (f *findScopeDashboardsREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
// See: /pkg/services/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) {
raw, err := f.scopeDashboardStorage.List(ctx, &internalversion.ListOptions{
Limit: 10000,
})
if err != nil {
w.WriteHeader(500)
return
}
all, ok := raw.(*scope.ScopeDashboardBindingList)
if !ok {
w.WriteHeader(500)
return
}
scopes := req.URL.Query()["scope"]
results := &scope.FindScopeDashboardBindingsResults{
Message: fmt.Sprintf("Find: %s", scopes),
Items: make([]scope.ScopeDashboardBinding, 0),
}
// we can improve the performance by calling .List once per scope if they are index by labels.
// The API stays the same thou.
for _, item := range all.Items {
for _, s := range scopes {
if item.Spec.Scope == s {
results.Items = append(results.Items, item)
}
}
}
// sort the dashboard lists based on dashboard title.
slices.SortFunc(results.Items, func(i, j scope.ScopeDashboardBinding) int {
return strings.Compare(i.Status.DashboardTitle, j.Status.DashboardTitle)
})
logger.FromContext(req.Context()).Debug("find scopedashboardbinding", "raw", len(all.Items), "filtered", len(results.Items), "scopeQueryParams", strings.Join(scopes, ","))
responder.Object(200, results)
}), nil
}
-71
View File
@@ -1,71 +0,0 @@
package scope
import (
"testing"
scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1"
"github.com/stretchr/testify/require"
)
func TestFilterAndAppendItem(t *testing.T) {
tcs := []struct {
Description string
ParentName string
Title string
QueryParam string
ParentParam string
ExpectedMatches int
}{
{
Description: "Matching parent without query param",
ParentName: "ParentNumberOne",
Title: "item",
QueryParam: "",
ParentParam: "ParentNumberOne",
ExpectedMatches: 1,
},
{
Description: "Not matching parent",
ParentName: "ParentNumberOne",
Title: "itemOne",
QueryParam: "itemTwo",
ParentParam: "ParentNumberTwo",
ExpectedMatches: 0,
},
{
Description: "Matching parent and query param",
ParentName: "ParentNumberOne",
Title: "itemOne",
QueryParam: "itemOne",
ParentParam: "ParentNumberOne",
ExpectedMatches: 1,
},
{
Description: "matching parent but not matching query param",
ParentName: "ParentNumberOne",
Title: "itemOne",
QueryParam: "itemTwo",
ParentParam: "ParentNumberOne",
ExpectedMatches: 0,
},
}
for _, tc := range tcs {
results := &scope.FindScopeNodeChildrenResults{}
item := scope.ScopeNode{
Spec: scope.ScopeNodeSpec{
ParentName: tc.ParentName,
Title: tc.Title,
Description: "item description",
NodeType: "item type",
LinkType: "item link type",
LinkID: "item link ID",
},
}
filterAndAppendItem(item, tc.ParentParam, tc.QueryParam, results)
require.Equal(t, len(results.Items), tc.ExpectedMatches, tc.Description)
}
}
-224
View File
@@ -1,224 +0,0 @@
package scope
import (
"fmt"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
var _ builder.APIGroupBuilder = (*ScopeAPIBuilder)(nil)
// This is used just so wire has something unique to return
type ScopeAPIBuilder struct{}
func NewScopeAPIBuilder() *ScopeAPIBuilder {
return &ScopeAPIBuilder{}
}
func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, reg prometheus.Registerer) *ScopeAPIBuilder {
if !featuremgmt.AnyEnabled(features,
featuremgmt.FlagScopeApi,
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
return nil // skip registration unless opting into experimental apis
}
builder := NewScopeAPIBuilder()
apiregistration.RegisterAPI(builder)
return builder
}
func (b *ScopeAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return nil // default authorizer is fine
}
func (b *ScopeAPIBuilder) GetGroupVersion() schema.GroupVersion {
return scope.SchemeGroupVersion
}
func (b *ScopeAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
err := scope.AddToScheme(scheme)
if err != nil {
return err
}
err = scheme.AddFieldLabelConversionFunc(
scope.ScopeResourceInfo.GroupVersionKind(),
func(label, value string) (string, string, error) {
fieldSet := SelectableScopeFields(&scope.Scope{})
for key := range fieldSet {
if label == key {
return label, value, nil
}
}
return "", "", fmt.Errorf("field label not supported for %s: %s", scope.ScopeResourceInfo.GroupVersionKind(), label)
},
)
if err != nil {
return err
}
err = scheme.AddFieldLabelConversionFunc(
scope.ScopeDashboardBindingResourceInfo.GroupVersionKind(),
func(label, value string) (string, string, error) {
fieldSet := SelectableScopeDashboardBindingFields(&scope.ScopeDashboardBinding{})
for key := range fieldSet {
if label == key {
return label, value, nil
}
}
return "", "", fmt.Errorf("field label not supported for %s: %s", scope.ScopeDashboardBindingResourceInfo.GroupVersionKind(), label)
},
)
if err != nil {
return err
}
err = scheme.AddFieldLabelConversionFunc(
scope.ScopeNodeResourceInfo.GroupVersionKind(),
func(label, value string) (string, string, error) {
fieldSet := SelectableScopeNodeFields(&scope.ScopeNode{})
for key := range fieldSet {
if label == key {
return label, value, nil
}
}
return "", "", fmt.Errorf("field label not supported for %s: %s", scope.ScopeNodeResourceInfo.GroupVersionKind(), label)
},
)
if err != nil {
return err
}
// This is required for --server-side apply
err = scope.AddKnownTypes(scope.InternalGroupVersion, scheme)
if err != nil {
return err
}
// Only one version right now
return scheme.SetVersionPriority(scope.SchemeGroupVersion)
}
func (b *ScopeAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
scheme := opts.Scheme
optsGetter := opts.OptsGetter
scopeResourceInfo := scope.ScopeResourceInfo
scopeDashboardResourceInfo := scope.ScopeDashboardBindingResourceInfo
scopeNodeResourceInfo := scope.ScopeNodeResourceInfo
storage := map[string]rest.Storage{}
scopeStorage, err := newScopeStorage(scheme, optsGetter)
if err != nil {
return err
}
storage[scopeResourceInfo.StoragePath()] = scopeStorage
scopeDashboardStorage, scopedDashboardStatusStorage, err := newScopeDashboardBindingStorage(scheme, optsGetter)
if err != nil {
return err
}
storage[scopeDashboardResourceInfo.StoragePath()] = scopeDashboardStorage
storage[scopeDashboardResourceInfo.StoragePath()+"/status"] = scopedDashboardStatusStorage
scopeNodeStorage, err := newScopeNodeStorage(scheme, optsGetter)
if err != nil {
return err
}
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["scope_node_children"] = &findREST{scopeNodeStorage: 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["scope_dashboard_bindings"] = &findScopeDashboardsREST{scopeDashboardStorage: scopeDashboardStorage}
apiGroupInfo.VersionedResourcesStorageMap[scope.VERSION] = storage
return nil
}
func (b *ScopeAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return scope.GetOpenAPIDefinitions
}
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}/scope_node_children/{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,
Schema: spec.StringProperty().UniqueValues(),
},
},
}
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}/scope_node_children/{name}")
oas.Paths.Paths[root+"namespaces/{namespace}/find/scope_node_children"] = sub
}
findDashboardPath := oas.Paths.Paths[root+"namespaces/{namespace}/scope_dashboard_bindings/{name}"]
if findDashboardPath != 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,
},
},
}
findDashboardPath.Get.Description = "find scope dashboard bindings that match any of the given scopes."
findDashboardPath.Get.Parameters = []*spec3.Parameter{
{
ParameterProps: spec3.ParameterProps{
Name: "scope",
In: "query",
Description: "A scope name (id) to match against, this parameter may be repeated",
},
},
}
delete(oas.Paths.Paths, root+"namespaces/{namespace}/scope_dashboard_bindings/{name}")
oas.Paths.Paths[root+"namespaces/{namespace}/find/scope_dashboard_bindings"] = findDashboardPath
}
return oas, nil
}
-142
View File
@@ -1,142 +0,0 @@
package scope
import (
"fmt"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"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"
)
var _ grafanarest.Storage = (*storage)(nil)
type storage struct {
*genericregistry.Store
}
func newScopeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) {
resourceInfo := scope.ScopeResourceInfo
strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion())
store := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
PredicateFunc: Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
TableConvertor: resourceInfo.TableConverter(),
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
}
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}
if err := store.CompleteWithOptions(options); err != nil {
return nil, err
}
return &storage{Store: store}, nil
}
func newScopeDashboardBindingStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, *grafanaregistry.StatusREST, error) {
resourceInfo := scope.ScopeDashboardBindingResourceInfo
strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion())
store := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
PredicateFunc: Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
TableConvertor: resourceInfo.TableConverter(),
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
}
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}
if err := store.CompleteWithOptions(options); err != nil {
return nil, nil, err
}
statusStrategy := grafanaregistry.NewStatusStrategy(scheme, resourceInfo.GroupVersion())
statusREST := grafanaregistry.NewStatusREST(store, statusStrategy)
return &storage{Store: store}, statusREST, nil
}
func newScopeNodeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) {
resourceInfo := scope.ScopeNodeResourceInfo
strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion())
store := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
PredicateFunc: Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
TableConvertor: resourceInfo.TableConverter(),
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
}
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}
if err := store.CompleteWithOptions(options); err != nil {
return nil, err
}
return &storage{Store: store}, nil
}
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
if s, ok := obj.(*scope.Scope); ok {
return labels.Set(s.Labels), SelectableScopeFields(s), nil
}
if s, ok := obj.(*scope.ScopeDashboardBinding); ok {
return labels.Set(s.Labels), SelectableScopeDashboardBindingFields(s), nil
}
if s, ok := obj.(*scope.ScopeNode); ok {
return labels.Set(s.Labels), SelectableScopeNodeFields(s), nil
}
return nil, nil, fmt.Errorf("not a scope or ScopeDashboardBinding object")
}
// Matcher returns a generic.SelectionPredicate that matches on label and field selectors.
func Matcher(label labels.Selector, field fields.Selector) apistore.SelectionPredicate {
return apistore.SelectionPredicate{
Label: label,
Field: field,
GetAttrs: GetAttrs,
}
}
func SelectableScopeFields(obj *scope.Scope) fields.Set {
return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{
"spec.title": obj.Spec.Title,
})
}
func SelectableScopeDashboardBindingFields(obj *scope.ScopeDashboardBinding) fields.Set {
return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{
"spec.scope": obj.Spec.Scope,
})
}
func SelectableScopeNodeFields(obj *scope.ScopeNode) fields.Set {
parentName := ""
if obj != nil {
parentName = obj.Spec.ParentName
}
return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{
"spec.parentName": parentName,
})
}
-2
View File
@@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/iam"
"github.com/grafana/grafana/pkg/registry/apis/provisioning"
"github.com/grafana/grafana/pkg/registry/apis/query"
"github.com/grafana/grafana/pkg/registry/apis/scope"
"github.com/grafana/grafana/pkg/registry/apis/service"
"github.com/grafana/grafana/pkg/registry/apis/userstorage"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
@@ -42,7 +41,6 @@ var WireSet = wire.NewSet(
provisioning.RegisterAPIService,
service.RegisterAPIService,
query.RegisterAPIService,
scope.RegisterAPIService,
notifications.RegisterAPIService,
userstorage.RegisterAPIService,
)