K8s: Add resource type helper to avoid so many hardcoded names (#79344)

This commit is contained in:
Ryan McKinley 2023-12-11 12:03:48 -08:00 committed by GitHub
parent 5147bdeb4b
commit f69516bf47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 190 additions and 71 deletions

View File

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

View File

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

View File

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

87
pkg/apis/types.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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