Storage: Avoid relying on RequestInfo (#89635)

This commit is contained in:
Todd Treece 2024-06-25 10:06:03 -04:00 committed by GitHub
parent 4cf3ebbb3d
commit 71270f3203
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 227 additions and 358 deletions

View File

@ -0,0 +1,132 @@
package generic
import (
"context"
"fmt"
"strings"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/validation/path"
"k8s.io/apimachinery/pkg/runtime/schema"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
)
type Key struct {
Group string
Resource string
Namespace string
Name string
}
func ParseKey(key string) (*Key, error) {
// /<group>/<resource>[/namespaces/<namespace>][/<name>]
parts := strings.Split(key, "/")
if len(parts) < 3 {
return nil, fmt.Errorf("invalid key (expecting at least 2 parts): %s", key)
}
if parts[0] != "" {
return nil, fmt.Errorf("invalid key (expecting leading slash): %s", key)
}
k := &Key{
Group: parts[1],
Resource: parts[2],
}
if len(parts) == 3 {
return k, nil
}
if parts[3] != "namespaces" {
k.Name = parts[3]
return k, nil
}
if len(parts) < 5 {
return nil, fmt.Errorf("invalid key (expecting namespace after 'namespaces'): %s", key)
}
k.Namespace = parts[4]
if len(parts) == 5 {
return k, nil
}
k.Name = parts[5]
return k, nil
}
func (k *Key) String() string {
s := "/" + k.Group + "/" + k.Resource
if len(k.Namespace) > 0 {
s += "/namespaces/" + k.Namespace
}
if len(k.Name) > 0 {
s += "/" + k.Name
}
return s
}
func (k *Key) IsEqual(other *Key) bool {
return k.Group == other.Group &&
k.Resource == other.Resource &&
k.Namespace == other.Namespace &&
k.Name == other.Name
}
// KeyRootFunc is used by the generic registry store to construct the first portion of the storage key.
func KeyRootFunc(gr schema.GroupResource) func(ctx context.Context) string {
return func(ctx context.Context) string {
ns, _ := genericapirequest.NamespaceFrom(ctx)
key := &Key{
Group: gr.Group,
Resource: gr.Resource,
Namespace: ns,
}
return key.String()
}
}
// NamespaceKeyFunc is the default function for constructing storage paths to
// a resource relative to the given prefix enforcing namespace rules. If the
// context does not contain a namespace, it errors.
func NamespaceKeyFunc(gr schema.GroupResource) func(ctx context.Context, name string) (string, error) {
return func(ctx context.Context, name string) (string, error) {
ns, ok := genericapirequest.NamespaceFrom(ctx)
if !ok || len(ns) == 0 {
return "", apierrors.NewBadRequest("Namespace parameter required.")
}
if len(name) == 0 {
return "", apierrors.NewBadRequest("Name parameter required.")
}
if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 {
return "", apierrors.NewBadRequest(fmt.Sprintf("Name parameter invalid: %q: %s", name, strings.Join(msgs, ";")))
}
key := &Key{
Group: gr.Group,
Resource: gr.Resource,
Namespace: ns,
Name: name,
}
return key.String(), nil
}
}
// NoNamespaceKeyFunc is the default function for constructing storage paths
// to a resource relative to the given prefix without a namespace.
func NoNamespaceKeyFunc(ctx context.Context, prefix string, gr schema.GroupResource, name string) (string, error) {
if len(name) == 0 {
return "", apierrors.NewBadRequest("Name parameter required.")
}
if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 {
return "", apierrors.NewBadRequest(fmt.Sprintf("Name parameter invalid: %q: %s", name, strings.Join(msgs, ";")))
}
key := &Key{
Group: gr.Group,
Resource: gr.Resource,
Name: name,
}
return prefix + key.String(), nil
}

View File

@ -61,6 +61,8 @@ func NewStorage(
s := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
PredicateFunc: grafanaregistry.Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),

View File

@ -26,6 +26,8 @@ func newStorage(scheme *runtime.Scheme) (*storage, error) {
store := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
PredicateFunc: grafanaregistry.Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),

View File

@ -23,6 +23,8 @@ func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, le
store := &genericregistry.Store{
NewFunc: resource.NewFunc,
NewListFunc: resource.NewListFunc,
KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
PredicateFunc: grafanaregistry.Matcher,
DefaultQualifiedResource: resource.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),

View File

@ -28,6 +28,8 @@ func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*
store := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
PredicateFunc: grafanaregistry.Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),

View File

@ -24,6 +24,8 @@ func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, le
store := &genericregistry.Store{
NewFunc: resource.NewFunc,
NewListFunc: resource.NewListFunc,
KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
PredicateFunc: grafanaregistry.Matcher,
DefaultQualifiedResource: resource.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),

View File

@ -31,6 +31,8 @@ func newScopeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGette
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(),
@ -73,6 +75,8 @@ func newScopeDashboardBindingStorage(scheme *runtime.Scheme, optsGetter generic.
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(),
@ -115,6 +119,8 @@ func newScopeNodeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsG
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(),

View File

@ -28,6 +28,8 @@ func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*
store := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
PredicateFunc: grafanaregistry.Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),

View File

@ -23,12 +23,12 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/storagebackend"
"k8s.io/apiserver/pkg/storage/storagebackend/factory"
"k8s.io/klog/v2"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
entityStore "github.com/grafana/grafana/pkg/services/store/entity"
)
@ -74,16 +74,16 @@ func NewStorage(
// in seconds (0 means forever). If no error is returned and out is not nil, out will be
// set to the read value from database.
func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, out runtime.Object, ttl uint64) error {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return apierrors.NewInternalError(fmt.Errorf("could not get request info"))
k, err := grafanaregistry.ParseKey(key)
if err != nil {
return err
}
if err := s.Versioner().PrepareObjectForStorage(obj); err != nil {
return err
}
e, err := resourceToEntity(obj, requestInfo, s.codec)
e, err := resourceToEntity(obj, *k, s.codec)
if err != nil {
return err
}
@ -114,17 +114,9 @@ func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, ou
// current version of the object to avoid read operation from storage to get it.
// However, the implementations have to retry in case suggestion is stale.
func (s *Storage) Delete(ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions, validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object) error {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return apierrors.NewInternalError(fmt.Errorf("could not get request info"))
}
k := &entityStore.Key{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: requestInfo.Name,
Subresource: requestInfo.Subresource,
k, err := grafanaregistry.ParseKey(key)
if err != nil {
return err
}
previousVersion := int64(0)
@ -156,17 +148,9 @@ func (s *Storage) Delete(ctx context.Context, key string, out runtime.Object, pr
// If resource version is "0", this interface will get current object at given key
// and send it in an "ADDED" event, before watch starts.
func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return nil, apierrors.NewInternalError(fmt.Errorf("could not get request info"))
}
k := &entityStore.Key{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: requestInfo.Name,
Subresource: requestInfo.Subresource,
k, err := grafanaregistry.ParseKey(key)
if err != nil {
return nil, err
}
if opts.Predicate.Field != nil {
@ -288,21 +272,12 @@ func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOption
// The returned contents may be delayed, but it is guaranteed that they will
// match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'.
func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, objPtr runtime.Object) error {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return apierrors.NewInternalError(fmt.Errorf("could not get request info"))
}
k := &entityStore.Key{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: requestInfo.Name,
Subresource: requestInfo.Subresource,
k, err := grafanaregistry.ParseKey(key)
if err != nil {
return err
}
resourceVersion := int64(0)
var err error
if opts.ResourceVersion != "" {
resourceVersion, err = strconv.ParseInt(opts.ResourceVersion, 10, 64)
if err != nil {
@ -343,17 +318,9 @@ func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions,
// The returned contents may be delayed, but it is guaranteed that they will
// match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'.
func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return apierrors.NewInternalError(fmt.Errorf("could not get request info"))
}
k := &entityStore.Key{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: requestInfo.Name,
Subresource: requestInfo.Subresource,
k, err := grafanaregistry.ParseKey(key)
if err != nil {
return err
}
listPtr, err := meta.GetItemsPtr(listObj)
@ -519,17 +486,9 @@ func (s *Storage) GuaranteedUpdate(
tryUpdate storage.UpdateFunc,
cachedExistingObject runtime.Object,
) error {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return apierrors.NewInternalError(fmt.Errorf("could not get request info"))
}
k := &entityStore.Key{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: requestInfo.Name,
Subresource: requestInfo.Subresource,
k, err := grafanaregistry.ParseKey(key)
if err != nil {
return err
}
getErr := s.Get(ctx, k.String(), storage.GetOptions{}, destination)
@ -565,7 +524,7 @@ func (s *Storage) GuaranteedUpdate(
return apierrors.NewInternalError(fmt.Errorf("could not successfully update object. key=%s, err=%s", k.String(), err.Error()))
}
e, err := resourceToEntity(updatedObj, requestInfo, s.codec)
e, err := resourceToEntity(updatedObj, *k, s.codec)
if err != nil {
return err
}

View File

@ -1,166 +0,0 @@
package test
import (
"context"
"fmt"
"strings"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/storage"
"github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/services/user"
)
var _ storage.Interface = &RequestInfoWrapper{}
type RequestInfoWrapper struct {
store storage.Interface
gr schema.GroupResource
}
func (r *RequestInfoWrapper) setRequestInfo(ctx context.Context, key string) (context.Context, error) {
pkey, err := convertToParsedKey(key)
if err != nil {
return nil, err
}
ctx = appcontext.WithUser(ctx, &user.SignedInUser{
Login: "admin",
UserID: 1,
OrgID: 1,
})
return request.WithRequestInfo(ctx, &request.RequestInfo{
APIGroup: pkey.Group,
APIVersion: "v1",
Resource: pkey.Resource,
Subresource: "",
Namespace: pkey.Namespace,
Name: pkey.Name,
Parts: strings.Split(key, "/"),
IsResourceRequest: true,
}), nil
}
func (r *RequestInfoWrapper) Create(ctx context.Context, key string, obj runtime.Object, out runtime.Object, ttl uint64) error {
ctx, err := r.setRequestInfo(ctx, key)
if err != nil {
return err
}
return r.store.Create(ctx, key, obj, out, ttl)
}
func (r *RequestInfoWrapper) Delete(ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions, validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object) error {
ctx, err := r.setRequestInfo(ctx, key)
if err != nil {
return err
}
return r.store.Delete(ctx, key, out, preconditions, validateDeletion, cachedExistingObject)
}
func (r *RequestInfoWrapper) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) {
ctx, err := r.setRequestInfo(ctx, key)
if err != nil {
return nil, err
}
return r.store.Watch(ctx, key, opts)
}
func (r *RequestInfoWrapper) Get(ctx context.Context, key string, opts storage.GetOptions, objPtr runtime.Object) error {
ctx, err := r.setRequestInfo(ctx, key)
if err != nil {
return err
}
return r.store.Get(ctx, key, opts, objPtr)
}
func (r *RequestInfoWrapper) GetList(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error {
ctx, err := r.setRequestInfo(ctx, key)
if err != nil {
return err
}
return r.store.GetList(ctx, key, opts, listObj)
}
func (r *RequestInfoWrapper) GuaranteedUpdate(ctx context.Context, key string, destination runtime.Object, ignoreNotFound bool, preconditions *storage.Preconditions, tryUpdate storage.UpdateFunc, cachedExistingObject runtime.Object) error {
ctx, err := r.setRequestInfo(ctx, key)
if err != nil {
return err
}
return r.store.GuaranteedUpdate(ctx, key, destination, ignoreNotFound, preconditions, tryUpdate, cachedExistingObject)
}
func (r *RequestInfoWrapper) Count(key string) (int64, error) {
return r.store.Count(key)
}
func (r *RequestInfoWrapper) Versioner() storage.Versioner {
return r.store.Versioner()
}
func (r *RequestInfoWrapper) RequestWatchProgress(ctx context.Context) error {
return r.store.RequestWatchProgress(ctx)
}
type Key struct {
Group string
Resource string
Namespace string
Name string
}
func convertToParsedKey(key string) (*Key, error) {
// NOTE: the following supports the watcher tests that run against v1/pods
// Other than that, there are ambiguities in the key format that only field selector
// when set to use metadata.name can be used to bring clarity in the 3-segment case
// Cases handled below:
// namespace scoped:
// /<resource>/[<namespace>]/[<name>]
// /<resource>/[<namespace>]
//
// cluster scoped:
// /<resource>/[<name>]
// /<resource>
k := &Key{}
if !strings.HasPrefix(key, "/") {
key = "/" + key
}
parts := strings.SplitN(key, "/", 5)
if len(parts) < 2 {
return nil, fmt.Errorf("invalid key format: %s", key)
}
k.Resource = parts[1]
if len(parts) < 3 {
return k, nil
}
// figure out whether the key is namespace scoped or cluster scoped
if isTestNs(parts[2]) {
k.Namespace = parts[2]
if len(parts) >= 4 {
k.Name = parts[3]
}
} else {
k.Name = parts[2]
}
return k, nil
}
func isTestNs(part string) bool {
return strings.HasPrefix(part, "test-ns-") || strings.HasPrefix(part, "ns-") || strings.Index(part, "-ns") > 0
}

View File

@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/apitesting"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -20,11 +21,13 @@ import (
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/apis/example"
examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/storagebackend"
"k8s.io/apiserver/pkg/storage/storagebackend/factory"
storagetesting "k8s.io/apiserver/pkg/storage/testing"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/services/apiserver/storage/entity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
@ -129,7 +132,12 @@ func testSetup(t *testing.T, opts ...setupOption) (context.Context, storage.Inte
client,
setupOpts.codec,
func(obj runtime.Object) (string, error) {
return storage.NamespaceKeyFunc(setupOpts.resourcePrefix, obj)
accessor, err := meta.Accessor(obj)
if err != nil {
return "", err
}
keyFn := grafanaregistry.NamespaceKeyFunc(setupOpts.groupResource)
return keyFn(genericapirequest.WithNamespace(genericapirequest.NewContext(), accessor.GetNamespace()), accessor.GetName())
},
setupOpts.newFunc,
setupOpts.newListFunc,
@ -141,12 +149,7 @@ func testSetup(t *testing.T, opts ...setupOption) (context.Context, storage.Inte
ctx := context.Background()
wrappedStore := &RequestInfoWrapper{
store: store,
gr: setupOpts.groupResource,
}
return ctx, wrappedStore, destroyFunc, nil
return ctx, store, destroyFunc, nil
}
func TestIntegrationWatch(t *testing.T) {
@ -250,6 +253,7 @@ func TestIntegrationWatchContextCancel(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Skip("In maintenance")
ctx, store, destroyFunc, err := testSetup(t)
defer destroyFunc()
@ -323,6 +327,7 @@ func TestIntegrationSendInitialEventsBackwardCompatibility(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Skip("In maintenance")
ctx, store, destroyFunc, err := testSetup(t)
defer destroyFunc()

View File

@ -13,9 +13,9 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/grafana/pkg/apimachinery/utils"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
entityStore "github.com/grafana/grafana/pkg/services/store/entity"
)
@ -98,7 +98,7 @@ func EntityToRuntimeObject(rsp *entityStore.Entity, res runtime.Object, codec ru
return nil
}
func resourceToEntity(res runtime.Object, requestInfo *request.RequestInfo, codec runtime.Codec) (*entityStore.Entity, error) {
func resourceToEntity(res runtime.Object, k grafanaregistry.Key, codec runtime.Codec) (*entityStore.Entity, error) {
metaAccessor, err := meta.Accessor(res)
if err != nil {
return nil, err
@ -110,19 +110,13 @@ func resourceToEntity(res runtime.Object, requestInfo *request.RequestInfo, code
}
rv, _ := strconv.ParseInt(metaAccessor.GetResourceVersion(), 10, 64)
k := &entityStore.Key{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: metaAccessor.GetName(),
Subresource: requestInfo.Subresource,
}
// add the object's name to the provided key
k.Name = metaAccessor.GetName()
rsp := &entityStore.Entity{
Group: k.Group,
GroupVersion: requestInfo.APIVersion,
GroupVersion: res.GetObjectKind().GroupVersionKind().Version,
Resource: k.Resource,
Subresource: k.Subresource,
Namespace: k.Namespace,
Key: k.String(),
Name: k.Name,

View File

@ -10,9 +10,9 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/grafana/pkg/apis/playlist/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
entityStore "github.com/grafana/grafana/pkg/services/store/entity"
)
@ -30,7 +30,7 @@ func TestResourceToEntity(t *testing.T) {
Codecs := serializer.NewCodecFactory(Scheme)
testCases := []struct {
requestInfo *request.RequestInfo
key grafanaregistry.Key
resource runtime.Object
codec runtime.Codec
expectedKey string
@ -52,14 +52,17 @@ func TestResourceToEntity(t *testing.T) {
expectedBody []byte
}{
{
requestInfo: &request.RequestInfo{
APIGroup: "playlist.grafana.app",
APIVersion: "v0alpha1",
Resource: "playlists",
Namespace: "default",
Name: "test-name",
key: grafanaregistry.Key{
Group: "playlist.grafana.app",
Resource: "playlists",
Namespace: "default",
Name: "test-name",
},
resource: &v0alpha1.Playlist{
TypeMeta: metav1.TypeMeta{
APIVersion: "playlist.grafana.app/v0alpha1",
Kind: "Playlist",
},
ObjectMeta: metav1.ObjectMeta{
CreationTimestamp: createdAt,
Labels: map[string]string{"label1": "value1", "label2": "value2"},
@ -105,7 +108,7 @@ func TestResourceToEntity(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.resource.GetObjectKind().GroupVersionKind().Kind+" to entity conversion should succeed", func(t *testing.T) {
entity, err := resourceToEntity(tc.resource, tc.requestInfo, Codecs.LegacyCodec(v0alpha1.PlaylistResourceInfo.GroupVersion()))
entity, err := resourceToEntity(tc.resource, tc.key, Codecs.LegacyCodec(v0alpha1.PlaylistResourceInfo.GroupVersion()))
require.NoError(t, err)
assert.Equal(t, tc.expectedKey, entity.Key)
assert.Equal(t, tc.expectedName, entity.Name)

View File

@ -1,82 +0,0 @@
package entity
import (
"fmt"
"strings"
)
type Key struct {
Group string
Resource string
Namespace string
Name string
Subresource string
}
func ParseKey(key string) (*Key, error) {
// /<group>/<resource>[/namespaces/<namespace>][/<name>[/<subresource>]]
parts := strings.Split(key, "/")
if len(parts) < 3 {
return nil, fmt.Errorf("invalid key (expecting at least 2 parts): %s", key)
}
if parts[0] != "" {
return nil, fmt.Errorf("invalid key (expecting leading slash): %s", key)
}
k := &Key{
Group: parts[1],
Resource: parts[2],
}
if len(parts) == 3 {
return k, nil
}
if parts[3] != "namespaces" {
k.Name = parts[3]
if len(parts) > 4 {
k.Subresource = strings.Join(parts[4:], "/")
}
return k, nil
}
if len(parts) < 5 {
return nil, fmt.Errorf("invalid key (expecting namespace after 'namespaces'): %s", key)
}
k.Namespace = parts[4]
if len(parts) == 5 {
return k, nil
}
k.Name = parts[5]
if len(parts) > 6 {
k.Subresource = strings.Join(parts[6:], "/")
}
return k, nil
}
func (k *Key) String() string {
s := "/" + k.Group + "/" + k.Resource
if len(k.Namespace) > 0 {
s += "/namespaces/" + k.Namespace
}
if len(k.Name) > 0 {
s += "/" + k.Name
if len(k.Subresource) > 0 {
s += "/" + k.Subresource
}
}
return s
}
func (k *Key) IsEqual(other *Key) bool {
return k.Group == other.Group &&
k.Resource == other.Resource &&
k.Namespace == other.Namespace &&
k.Name == other.Name &&
k.Subresource == other.Subresource
}

View File

@ -8,6 +8,7 @@ import (
"github.com/google/uuid"
folder "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/store/entity/db"
"github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
@ -21,7 +22,7 @@ func (s *sqlEntityServer) Create(ctx context.Context, r *entity.CreateEntityRequ
return nil, err
}
key, err := entity.ParseKey(r.Entity.Key)
key, err := grafanaregistry.ParseKey(r.Entity.Key)
if err != nil {
return nil, fmt.Errorf("create entity: parse entity key: %w", err)
}
@ -98,7 +99,7 @@ func (s *sqlEntityServer) Create(ctx context.Context, r *entity.CreateEntityRequ
// entityForCreate validates the given request and returns a *returnsEntity
// populated accordingly.
func entityForCreate(ctx context.Context, r *entity.CreateEntityRequest, key *entity.Key) (*returnsEntity, error) {
func entityForCreate(ctx context.Context, r *entity.CreateEntityRequest, key *grafanaregistry.Key) (*returnsEntity, error) {
newEntity := &returnsEntity{
Entity: cloneEntity(r.Entity),
}

View File

@ -7,6 +7,7 @@ import (
"time"
folder "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/store/entity/db"
"github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
@ -20,7 +21,7 @@ func (s *sqlEntityServer) Delete(ctx context.Context, r *entity.DeleteEntityRequ
return nil, err
}
key, err := entity.ParseKey(r.Key)
key, err := grafanaregistry.ParseKey(r.Key)
if err != nil {
return nil, fmt.Errorf("delete entity: parse entity key: %w", err)
}

View File

@ -13,6 +13,7 @@ import (
"google.golang.org/protobuf/proto"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/store/entity/db"
"github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
@ -218,7 +219,7 @@ func (r sqlEntityListFolderElementsRequest) Validate() error {
// cases and proper database deserialization.
type sqlEntityReadRequest struct {
*sqltemplate.SQLTemplate
Key *entity.Key
Key *grafanaregistry.Key
ResourceVersion int64
SelectForUpdate bool
returnsEntitySet
@ -230,7 +231,7 @@ func (r sqlEntityReadRequest) Validate() error {
type sqlEntityDeleteRequest struct {
*sqltemplate.SQLTemplate
Key *entity.Key
Key *grafanaregistry.Key
}
func (r sqlEntityDeleteRequest) Validate() error {
@ -479,7 +480,7 @@ func readEntity(
ctx context.Context,
x db.ContextExecer,
d sqltemplate.Dialect,
k *entity.Key,
k *grafanaregistry.Key,
asOfVersion int64,
optimisticLocking bool,
selectForUpdate bool,

View File

@ -13,6 +13,7 @@ import (
sqlmock "github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/require"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/store/entity/db"
"github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
@ -107,7 +108,7 @@ func TestQueries(t *testing.T) {
Name: "single path",
Data: &sqlEntityDeleteRequest{
SQLTemplate: new(sqltemplate.SQLTemplate),
Key: new(entity.Key),
Key: new(grafanaregistry.Key),
},
Expected: expected{
"entity_delete_mysql_sqlite.sql": dialects{
@ -173,7 +174,7 @@ func TestQueries(t *testing.T) {
Name: "with resource version and select for update",
Data: &sqlEntityReadRequest{
SQLTemplate: new(sqltemplate.SQLTemplate),
Key: new(entity.Key),
Key: new(grafanaregistry.Key),
ResourceVersion: 1,
SelectForUpdate: true,
returnsEntitySet: returnsEntitySet{
@ -190,7 +191,7 @@ func TestQueries(t *testing.T) {
Name: "without resource version and select for update",
Data: &sqlEntityReadRequest{
SQLTemplate: new(sqltemplate.SQLTemplate),
Key: new(entity.Key),
Key: new(grafanaregistry.Key),
returnsEntitySet: returnsEntitySet{
Entity: newReturnsEntity(),
},
@ -547,7 +548,7 @@ func TestReadEntity(t *testing.T) {
// readonly, shared data for all subtests
expectedEntity := newEmptyEntity()
testdataJSON(t, `grpc-res-entity.json`, expectedEntity)
key, err := entity.ParseKey(expectedEntity.Key)
key, err := grafanaregistry.ParseKey(expectedEntity.Key)
require.NoErrorf(t, err, "provided key: %#v", expectedEntity)
t.Run("happy path - entity table, optimistic locking", func(t *testing.T) {
@ -567,7 +568,7 @@ func TestReadEntity(t *testing.T) {
db, mock := newMockDBMatchWords(t)
readReq := sqlEntityReadRequest{ // used to generate mock results
SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
Key: new(entity.Key),
Key: new(grafanaregistry.Key),
returnsEntitySet: newReturnsEntitySet(),
}
readReq.Entity.Entity = cloneEntity(expectedEntity)
@ -592,7 +593,7 @@ func TestReadEntity(t *testing.T) {
db, mock := newMockDBMatchWords(t)
readReq := sqlEntityReadRequest{ // used to generate mock results
SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
Key: new(entity.Key),
Key: new(grafanaregistry.Key),
returnsEntitySet: newReturnsEntitySet(),
}
readReq.Entity.Entity = cloneEntity(expectedEntity)
@ -627,7 +628,7 @@ func TestReadEntity(t *testing.T) {
db, mock := newMockDBMatchWords(t)
readReq := sqlEntityReadRequest{ // used to generate mock results
SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
Key: new(entity.Key),
Key: new(grafanaregistry.Key),
returnsEntitySet: newReturnsEntitySet(),
}
results := newMockResults(t, mock, sqlEntityRead, readReq)
@ -652,7 +653,7 @@ func TestReadEntity(t *testing.T) {
db, mock := newMockDBMatchWords(t)
readReq := sqlEntityReadRequest{ // used to generate mock results
SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
Key: new(entity.Key),
Key: new(grafanaregistry.Key),
returnsEntitySet: newReturnsEntitySet(),
}
readReq.Entity.Entity = cloneEntity(expectedEntity)
@ -684,7 +685,7 @@ func expectReadEntity(t *testing.T, mock sqlmock.Sqlmock, e *entity.Entity) func
// test declarations
readReq := sqlEntityReadRequest{ // used to generate mock results
SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
Key: new(entity.Key),
Key: new(grafanaregistry.Key),
returnsEntitySet: newReturnsEntitySet(),
}
results := newMockResults(t, mock, sqlEntityRead, readReq)

View File

@ -18,6 +18,7 @@ import (
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/apimachinery/identity"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
@ -302,7 +303,7 @@ func (s *sqlEntityServer) read(ctx context.Context, tx session.SessionQuerier, r
return nil, fmt.Errorf("missing key")
}
key, err := entity.ParseKey(r.Key)
key, err := grafanaregistry.ParseKey(r.Key)
if err != nil {
return nil, err
}
@ -395,7 +396,7 @@ func (s *sqlEntityServer) history(ctx context.Context, r *entity.EntityHistoryRe
entityQuery.AddFields(fields...)
if r.Key != "" {
key, err := entity.ParseKey(r.Key)
key, err := grafanaregistry.ParseKey(r.Key)
if err != nil {
return nil, err
}
@ -629,7 +630,7 @@ func (s *sqlEntityServer) List(ctx context.Context, r *entity.EntityListRequest)
where := []string{}
args := []any{}
for _, k := range r.Key {
key, err := entity.ParseKey(k)
key, err := grafanaregistry.ParseKey(k)
if err != nil {
return nil, err
}
@ -868,7 +869,7 @@ func (s *sqlEntityServer) watchInit(ctx context.Context, r *entity.EntityWatchRe
where := []string{}
args := []any{}
for _, k := range r.Key {
key, err := entity.ParseKey(k)
key, err := grafanaregistry.ParseKey(k)
if err != nil {
ctxLogger.Error("error parsing key", "error", err, "key", k)
return lastRv, err
@ -1153,7 +1154,7 @@ func watchMatches(r *entity.EntityWatchRequest, result *entity.Entity) bool {
if len(r.Key) > 0 {
matched := false
for _, k := range r.Key {
key, err := entity.ParseKey(k)
key, err := grafanaregistry.ParseKey(k)
if err != nil {
return false
}

View File

@ -8,6 +8,7 @@ import (
"time"
folder "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/store/entity/db"
"github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
@ -21,7 +22,7 @@ func (s *sqlEntityServer) Update(ctx context.Context, r *entity.UpdateEntityRequ
return nil, err
}
key, err := entity.ParseKey(r.Entity.Key)
key, err := grafanaregistry.ParseKey(r.Entity.Key)
if err != nil {
return nil, fmt.Errorf("update entity: parse entity key: %w", err)
}