From f69516bf47795d4dee8af3d57790a2c91de9ab7b Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 11 Dec 2023 12:03:48 -0800 Subject: [PATCH] K8s: Add resource type helper to avoid so many hardcoded names (#79344) --- pkg/api/playlist.go | 7 +- pkg/apis/example/v0alpha1/types.go | 20 +++++ pkg/apis/playlist/v0alpha1/types.go | 15 ++++ pkg/apis/types.go | 87 +++++++++++++++++++ pkg/registry/apis/example/dummy_storage.go | 13 +-- pkg/registry/apis/example/register.go | 17 ++-- pkg/registry/apis/example/storage.go | 18 ++-- pkg/registry/apis/playlist/legacy_storage.go | 23 ++--- pkg/registry/apis/playlist/register.go | 38 ++++---- pkg/registry/apis/playlist/storage.go | 9 +- .../endpoints/request/namespace.go | 14 +++ 11 files changed, 190 insertions(+), 71 deletions(-) create mode 100644 pkg/apis/types.go diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 9c2f0f94ff9..4a49bc59708 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" "github.com/grafana/grafana/pkg/middleware" internalplaylist "github.com/grafana/grafana/pkg/registry/apis/playlist" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -329,11 +330,7 @@ type playlistK8sHandler struct { func newPlaylistK8sHandler(hs *HTTPServer) *playlistK8sHandler { return &playlistK8sHandler{ - gvr: schema.GroupVersionResource{ - Group: internalplaylist.GroupName, - Version: "v0alpha1", - Resource: "playlists", - }, + gvr: v0alpha1.PlaylistResourceInfo.GroupVersionResource(), namespacer: request.GetNamespaceMapper(hs.Cfg), clientConfigProvider: hs.clientConfigProvider, } diff --git a/pkg/apis/example/v0alpha1/types.go b/pkg/apis/example/v0alpha1/types.go index ada5a2c2def..343bd78738b 100644 --- a/pkg/apis/example/v0alpha1/types.go +++ b/pkg/apis/example/v0alpha1/types.go @@ -2,6 +2,26 @@ package v0alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + + "github.com/grafana/grafana/pkg/apis" +) + +const ( + GROUP = "example.grafana.app" + VERSION = "v0alpha1" + APIVERSION = GROUP + "/" + VERSION +) + +var RuntimeResourceInfo = apis.NewResourceInfo(GROUP, VERSION, + "runtime", "runtime", "RuntimeInfo", + func() runtime.Object { return &RuntimeInfo{} }, + func() runtime.Object { return &RuntimeInfo{} }, +) +var DummyResourceInfo = apis.NewResourceInfo(GROUP, VERSION, + "dummy", "dummy", "DummyResource", + func() runtime.Object { return &DummyResource{} }, + func() runtime.Object { return &DummyResourceList{} }, ) // Mirrors the info exposed in "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/apis/playlist/v0alpha1/types.go b/pkg/apis/playlist/v0alpha1/types.go index fb5b9e6faa6..b34a52cb634 100644 --- a/pkg/apis/playlist/v0alpha1/types.go +++ b/pkg/apis/playlist/v0alpha1/types.go @@ -2,6 +2,21 @@ package v0alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + + "github.com/grafana/grafana/pkg/apis" +) + +const ( + GROUP = "playlist.grafana.app" + VERSION = "v0alpha1" + APIVERSION = GROUP + "/" + VERSION +) + +var PlaylistResourceInfo = apis.NewResourceInfo(GROUP, VERSION, + "playlists", "playlist", "Playlist", + func() runtime.Object { return &Playlist{} }, + func() runtime.Object { return &PlaylistList{} }, ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/types.go b/pkg/apis/types.go new file mode 100644 index 00000000000..fdb1ff9b65b --- /dev/null +++ b/pkg/apis/types.go @@ -0,0 +1,87 @@ +package apis + +import ( + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// ResourceInfo helps define a k8s resource +type ResourceInfo struct { + group string + version string + resourceName string + singularName string + kind string + newObj func() runtime.Object + newList func() runtime.Object +} + +func NewResourceInfo(group, version, resourceName, singularName, kind string, + newObj func() runtime.Object, newList func() runtime.Object) ResourceInfo { + return ResourceInfo{group, version, resourceName, singularName, kind, newObj, newList} +} + +func (info *ResourceInfo) GetSingularName() string { + return info.singularName +} + +// TypeMeta returns k8s type +func (info *ResourceInfo) TypeMeta() metav1.TypeMeta { + return metav1.TypeMeta{ + Kind: info.kind, + APIVersion: info.group + "/" + info.version, + } +} + +func (info *ResourceInfo) GroupVersion() schema.GroupVersion { + return schema.GroupVersion{ + Group: info.group, + Version: info.version, + } +} + +func (info *ResourceInfo) GroupResource() schema.GroupResource { + return schema.GroupResource{ + Group: info.group, + Resource: info.resourceName, + } +} + +func (info *ResourceInfo) SingularGroupResource() schema.GroupResource { + return schema.GroupResource{ + Group: info.group, + Resource: info.singularName, + } +} + +func (info *ResourceInfo) GroupVersionResource() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: info.group, + Version: info.version, + Resource: info.resourceName, + } +} + +func (info *ResourceInfo) StoragePath(sub ...string) string { + switch len(sub) { + case 0: + return info.resourceName + case 1: + return info.resourceName + "/" + sub[0] + } + panic("invalid subresource path") +} + +func (info *ResourceInfo) NewFunc() runtime.Object { + return info.newObj() +} + +func (info *ResourceInfo) NewListFunc() runtime.Object { + return info.newList() +} + +func (info *ResourceInfo) NewNotFound(name string) *errors.StatusError { + return errors.NewNotFound(info.SingularGroupResource(), name) +} diff --git a/pkg/registry/apis/example/dummy_storage.go b/pkg/registry/apis/example/dummy_storage.go index 0099b744125..290623771cb 100644 --- a/pkg/registry/apis/example/dummy_storage.go +++ b/pkg/registry/apis/example/dummy_storage.go @@ -32,18 +32,19 @@ type dummyStorage struct { } func newDummyStorage(gv schema.GroupVersion, scheme *runtime.Scheme, names ...string) *dummyStorage { + var resourceInfo = example.DummyResourceInfo strategy := grafanaregistry.NewStrategy(scheme) store := &genericregistry.Store{ - NewFunc: func() runtime.Object { return &example.DummyResource{} }, // getter not supported - NewListFunc: func() runtime.Object { return &example.DummyResourceList{} }, // both list and get return the same thing + NewFunc: resourceInfo.NewFunc, + NewListFunc: resourceInfo.NewListFunc, PredicateFunc: grafanaregistry.Matcher, - DefaultQualifiedResource: gv.WithResource("dummy").GroupResource(), - SingularQualifiedResource: gv.WithResource("dummy").GroupResource(), + DefaultQualifiedResource: resourceInfo.GroupResource(), + SingularQualifiedResource: resourceInfo.SingularGroupResource(), + TableConvertor: rest.NewDefaultTableConvertor(resourceInfo.GroupResource()), CreateStrategy: strategy, UpdateStrategy: strategy, DeleteStrategy: strategy, } - store.TableConvertor = rest.NewDefaultTableConvertor(store.DefaultQualifiedResource) return &dummyStorage{ store: store, @@ -63,7 +64,7 @@ func (s *dummyStorage) NamespaceScoped() bool { } func (s *dummyStorage) GetSingularName() string { - return "dummy" + return example.DummyResourceInfo.GetSingularName() } func (s *dummyStorage) NewList() runtime.Object { diff --git a/pkg/registry/apis/example/register.go b/pkg/registry/apis/example/register.go index 509e3ac40c1..4d2a97e6ab2 100644 --- a/pkg/registry/apis/example/register.go +++ b/pkg/registry/apis/example/register.go @@ -23,11 +23,6 @@ import ( grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" ) -// GroupName is the group name for this API. -const GroupName = "example.grafana.app" -const VersionID = "v0alpha1" // -const APIVersion = GroupName + "/" + VersionID - var _ grafanaapiserver.APIGroupBuilder = (*TestingAPIBuilder)(nil) // This is used just so wire has something unique to return @@ -38,7 +33,7 @@ type TestingAPIBuilder struct { func NewTestingAPIBuilder() *TestingAPIBuilder { return &TestingAPIBuilder{ - gv: schema.GroupVersion{Group: GroupName, Version: VersionID}, + gv: schema.GroupVersion{Group: example.GROUP, Version: example.VERSION}, } } @@ -89,13 +84,13 @@ func (b *TestingAPIBuilder) GetAPIGroupInfo( optsGetter generic.RESTOptionsGetter, ) (*genericapiserver.APIGroupInfo, error) { b.codecs = codecs - apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(GroupName, scheme, metav1.ParameterCodec, codecs) + apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(b.gv.Group, scheme, metav1.ParameterCodec, codecs) storage := map[string]rest.Storage{} - storage["runtime"] = newDeploymentInfoStorage(b.gv, scheme) - storage["dummy"] = newDummyStorage(b.gv, scheme, "test1", "test2", "test3") - storage["dummy/sub"] = &dummySubresourceREST{} - apiGroupInfo.VersionedResourcesStorageMap[VersionID] = storage + storage[example.RuntimeResourceInfo.StoragePath()] = newDeploymentInfoStorage(b.gv, scheme) + storage[example.DummyResourceInfo.StoragePath()] = newDummyStorage(b.gv, scheme, "test1", "test2", "test3") + storage[example.DummyResourceInfo.StoragePath("sub")] = &dummySubresourceREST{} + apiGroupInfo.VersionedResourcesStorageMap[b.gv.Version] = storage return &apiGroupInfo, nil } diff --git a/pkg/registry/apis/example/storage.go b/pkg/registry/apis/example/storage.go index 605e3b73db8..fbec2ad7316 100644 --- a/pkg/registry/apis/example/storage.go +++ b/pkg/registry/apis/example/storage.go @@ -29,26 +29,24 @@ type staticStorage struct { } func newDeploymentInfoStorage(gv schema.GroupVersion, scheme *runtime.Scheme) *staticStorage { + var resourceInfo = example.RuntimeResourceInfo strategy := grafanaregistry.NewStrategy(scheme) store := &genericregistry.Store{ - NewFunc: func() runtime.Object { return &example.RuntimeInfo{} }, // getter not supported - NewListFunc: func() runtime.Object { return &example.RuntimeInfo{} }, // both list and get return the same thing + NewFunc: resourceInfo.NewFunc, + NewListFunc: resourceInfo.NewListFunc, PredicateFunc: grafanaregistry.Matcher, - DefaultQualifiedResource: gv.WithResource("runtime").GroupResource(), - SingularQualifiedResource: gv.WithResource("runtime").GroupResource(), + DefaultQualifiedResource: resourceInfo.GroupResource(), + SingularQualifiedResource: resourceInfo.SingularGroupResource(), + TableConvertor: rest.NewDefaultTableConvertor(resourceInfo.GroupResource()), CreateStrategy: strategy, UpdateStrategy: strategy, DeleteStrategy: strategy, } - store.TableConvertor = rest.NewDefaultTableConvertor(store.DefaultQualifiedResource) return &staticStorage{ Store: store, info: example.RuntimeInfo{ - TypeMeta: metav1.TypeMeta{ - APIVersion: APIVersion, - Kind: "DeploymentInfo", - }, + TypeMeta: example.RuntimeResourceInfo.TypeMeta(), BuildVersion: setting.BuildVersion, BuildCommit: setting.BuildCommit, BuildBranch: setting.BuildBranch, @@ -72,7 +70,7 @@ func (s *staticStorage) NamespaceScoped() bool { } func (s *staticStorage) GetSingularName() string { - return "runtime" + return example.RuntimeResourceInfo.GetSingularName() } func (s *staticStorage) NewList() runtime.Object { diff --git a/pkg/registry/apis/playlist/legacy_storage.go b/pkg/registry/apis/playlist/legacy_storage.go index d5c962343ec..8b7f6b4b18d 100644 --- a/pkg/registry/apis/playlist/legacy_storage.go +++ b/pkg/registry/apis/playlist/legacy_storage.go @@ -5,11 +5,9 @@ import ( "errors" "fmt" - k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/registry/rest" playlist "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" @@ -28,17 +26,16 @@ var ( _ rest.GracefulDeleter = (*legacyStorage)(nil) ) +var resourceInfo = playlist.PlaylistResourceInfo + type legacyStorage struct { service playlistsvc.Service namespacer request.NamespaceMapper tableConverter rest.TableConvertor - - DefaultQualifiedResource schema.GroupResource - SingularQualifiedResource schema.GroupResource } func (s *legacyStorage) New() runtime.Object { - return &playlist.Playlist{} + return resourceInfo.NewFunc() } func (s *legacyStorage) Destroy() {} @@ -48,11 +45,11 @@ func (s *legacyStorage) NamespaceScoped() bool { } func (s *legacyStorage) GetSingularName() string { - return "playlist" + return resourceInfo.GetSingularName() } func (s *legacyStorage) NewList() runtime.Object { - return &playlist.PlaylistList{} + return resourceInfo.NewListFunc() } func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { @@ -60,9 +57,7 @@ func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Objec } func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - // TODO: handle fetching all available orgs when no namespace is specified - // To test: kubectl get playlists --all-namespaces - info, err := request.NamespaceInfoFrom(ctx, true) + orgId, err := request.OrgIDForList(ctx) if err != nil { return nil, err } @@ -72,7 +67,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO limit = int(options.Limit) } res, err := s.service.Search(ctx, &playlistsvc.GetPlaylistsQuery{ - OrgId: info.OrgID, + OrgId: orgId, Limit: limit, }) if err != nil { @@ -83,7 +78,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO for _, v := range res { p, err := s.service.Get(ctx, &playlistsvc.GetPlaylistByUidQuery{ UID: v.UID, - OrgId: info.OrgID, + OrgId: orgId, }) if err != nil { return nil, err @@ -108,7 +103,7 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge }) if err != nil || dto == nil { if errors.Is(err, playlistsvc.ErrPlaylistNotFound) || err == nil { - err = k8serrors.NewNotFound(s.SingularQualifiedResource, name) + err = resourceInfo.NewNotFound(name) } return nil, err } diff --git a/pkg/registry/apis/playlist/register.go b/pkg/registry/apis/playlist/register.go index 47ea4a34c23..be4c544227b 100644 --- a/pkg/registry/apis/playlist/register.go +++ b/pkg/registry/apis/playlist/register.go @@ -22,10 +22,6 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -// GroupName is the group name for this API. -const GroupName = "playlist.grafana.app" -const VersionID = "v0alpha1" - var _ grafanaapiserver.APIGroupBuilder = (*PlaylistAPIBuilder)(nil) // This is used just so wire has something unique to return @@ -42,7 +38,7 @@ func RegisterAPIService(p playlistsvc.Service, builder := &PlaylistAPIBuilder{ service: p, namespacer: request.GetNamespaceMapper(cfg), - gv: schema.GroupVersion{Group: GroupName, Version: VersionID}, + gv: playlist.PlaylistResourceInfo.GroupVersion(), } apiregistration.RegisterAPI(builder) return builder @@ -52,22 +48,23 @@ func (b *PlaylistAPIBuilder) GetGroupVersion() schema.GroupVersion { return b.gv } -func (b *PlaylistAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { - scheme.AddKnownTypes(b.gv, +func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { + scheme.AddKnownTypes(gv, &playlist.Playlist{}, &playlist.PlaylistList{}, ) +} + +func (b *PlaylistAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { + addKnownTypes(scheme, b.gv) // Link this version to the internal representation. // This is used for server-side-apply (PATCH), and avoids the error: // "no kind is registered for the type" - scheme.AddKnownTypes(schema.GroupVersion{ + addKnownTypes(scheme, schema.GroupVersion{ Group: b.gv.Group, Version: runtime.APIVersionInternal, - }, - &playlist.Playlist{}, - &playlist.PlaylistList{}, - ) + }) // If multiple versions exist, then register conversions from zz_generated.conversion.go // if err := playlist.RegisterConversions(scheme); err != nil { @@ -82,17 +79,16 @@ func (b *PlaylistAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, // pointer? optsGetter generic.RESTOptionsGetter, ) (*genericapiserver.APIGroupInfo, error) { - apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(GroupName, scheme, metav1.ParameterCodec, codecs) + apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(playlist.GROUP, scheme, metav1.ParameterCodec, codecs) storage := map[string]rest.Storage{} + resource := playlist.PlaylistResourceInfo legacyStore := &legacyStorage{ - service: b.service, - namespacer: b.namespacer, - DefaultQualifiedResource: b.gv.WithResource("playlists").GroupResource(), - SingularQualifiedResource: b.gv.WithResource("playlist").GroupResource(), + service: b.service, + namespacer: b.namespacer, } legacyStore.tableConverter = utils.NewTableConverter( - legacyStore.DefaultQualifiedResource, + resource.GroupResource(), []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, {Name: "Title", Type: "string", Format: "string", Description: "The playlist name"}, @@ -112,7 +108,7 @@ func (b *PlaylistAPIBuilder) GetAPIGroupInfo( }, nil }, ) - storage["playlists"] = legacyStore + storage[resource.StoragePath()] = legacyStore // enable dual writes if a RESTOptionsGetter is provided if optsGetter != nil { @@ -120,10 +116,10 @@ func (b *PlaylistAPIBuilder) GetAPIGroupInfo( if err != nil { return nil, err } - storage["playlists"] = grafanarest.NewDualWriter(legacyStore, store) + storage[resource.StoragePath()] = grafanarest.NewDualWriter(legacyStore, store) } - apiGroupInfo.VersionedResourcesStorageMap[VersionID] = storage + apiGroupInfo.VersionedResourcesStorageMap[playlist.VERSION] = storage return &apiGroupInfo, nil } diff --git a/pkg/registry/apis/playlist/storage.go b/pkg/registry/apis/playlist/storage.go index bd723d34b2f..b0f848cc11f 100644 --- a/pkg/registry/apis/playlist/storage.go +++ b/pkg/registry/apis/playlist/storage.go @@ -19,12 +19,13 @@ type storage struct { func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, legacy *legacyStorage) (*storage, error) { strategy := grafanaregistry.NewStrategy(scheme) + resource := playlist.PlaylistResourceInfo store := &genericregistry.Store{ - NewFunc: func() runtime.Object { return &playlist.Playlist{} }, - NewListFunc: func() runtime.Object { return &playlist.PlaylistList{} }, + NewFunc: resource.NewFunc, + NewListFunc: resource.NewListFunc, PredicateFunc: grafanaregistry.Matcher, - DefaultQualifiedResource: legacy.DefaultQualifiedResource, - SingularQualifiedResource: legacy.SingularQualifiedResource, + DefaultQualifiedResource: resource.GroupResource(), + SingularQualifiedResource: resourceInfo.SingularGroupResource(), TableConvertor: legacy.tableConverter, CreateStrategy: strategy, diff --git a/pkg/services/grafana-apiserver/endpoints/request/namespace.go b/pkg/services/grafana-apiserver/endpoints/request/namespace.go index c35ace9ce0f..ccec8680f81 100644 --- a/pkg/services/grafana-apiserver/endpoints/request/namespace.go +++ b/pkg/services/grafana-apiserver/endpoints/request/namespace.go @@ -8,6 +8,7 @@ import ( "k8s.io/apiserver/pkg/endpoints/request" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/setting" ) @@ -75,3 +76,16 @@ func ParseNamespace(ns string) (NamespaceInfo, error) { } return info, nil } + +func OrgIDForList(ctx context.Context) (int64, error) { + ns := request.NamespaceValue(ctx) + if ns == "" { + user, err := appcontext.User(ctx) + if user != nil { + return user.OrgID, err + } + return -1, err + } + info, err := ParseNamespace(ns) + return info.OrgID, err +}