K8s: Add storage dual writer (#75403)

This commit is contained in:
Todd Treece
2023-09-26 17:15:15 -04:00
committed by GitHub
parent ebec452f9f
commit bb9e66e671
19 changed files with 553 additions and 195 deletions

View File

@@ -0,0 +1,5 @@
// +k8s:deepcopy-gen=package
// +k8s:openapi-gen=true
// +groupName=playlist.grafana.io
package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1"

View File

@@ -0,0 +1,128 @@
package v0alpha1
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
grafanarequest "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/playlist"
)
var (
_ rest.Scoper = (*legacyStorage)(nil)
_ rest.SingularNameProvider = (*legacyStorage)(nil)
_ rest.Getter = (*legacyStorage)(nil)
_ rest.Lister = (*legacyStorage)(nil)
_ rest.Storage = (*legacyStorage)(nil)
)
type legacyStorage struct {
service playlist.Service
}
func newLegacyStorage(s playlist.Service) *legacyStorage {
return &legacyStorage{
service: s,
}
}
func (s *legacyStorage) New() runtime.Object {
return &Playlist{}
}
func (s *legacyStorage) Destroy() {}
func (s *legacyStorage) NamespaceScoped() bool {
return true // namespace == org
}
func (s *legacyStorage) GetSingularName() string {
return "playlist"
}
func (s *legacyStorage) NewList() runtime.Object {
return &PlaylistList{}
}
func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return rest.NewDefaultTableConvertor(Resource("playlists")).ConvertToTable(ctx, object, tableOptions)
}
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
orgId, ok := grafanarequest.OrgIDFrom(ctx)
if !ok {
orgId = 1 // TODO: default org ID 1 for now
}
limit := 100
if options.Limit > 0 {
limit = int(options.Limit)
}
res, err := s.service.Search(ctx, &playlist.GetPlaylistsQuery{
OrgId: orgId,
Limit: limit,
})
if err != nil {
return nil, err
}
list := &PlaylistList{
TypeMeta: metav1.TypeMeta{
Kind: "PlaylistList",
APIVersion: APIVersion,
},
}
for _, v := range res {
p := Playlist{
TypeMeta: metav1.TypeMeta{
Kind: "Playlist",
APIVersion: APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: v.UID,
},
}
p.Name = v.Name + " // " + v.Interval
list.Items = append(list.Items, p)
}
if len(list.Items) == limit {
list.Continue = "<more>" // TODO?
}
return list, nil
}
func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
orgId, ok := grafanarequest.OrgIDFrom(ctx)
if !ok {
orgId = 1 // TODO: default org ID 1 for now
}
p, err := s.service.Get(ctx, &playlist.GetPlaylistByUidQuery{
UID: name,
OrgId: orgId,
})
if err != nil {
return nil, err
}
if p == nil {
return nil, fmt.Errorf("not found?")
}
return &Playlist{
TypeMeta: metav1.TypeMeta{
Kind: "Playlist",
APIVersion: APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: p.Uid,
},
Name: p.Name + "//" + p.Interval,
}, nil
}

View File

@@ -0,0 +1,115 @@
package v0alpha1
import (
common "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec"
)
// NOTE: this must match the golang fully qualifid name!
const kindKey = "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Playlist"
func getOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
kindKey: schema_pkg_Playlist(ref),
kindKey + "List": schema_pkg_PlaylistList(ref),
}
}
func schema_pkg_Playlist(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Playlist",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
// TODO! add a real spec here
// "spec": {
// SchemaProps: spec.SchemaProps{
// Default: map[string]interface{}{},
// Ref: ref("github.com/grafana/google-sheets-datasource/pkg/apis/googlesheets/v1.DatasourceSpec"),
// },
// },
// "status": {
// SchemaProps: spec.SchemaProps{
// Default: map[string]interface{}{},
// Ref: ref("github.com/grafana/google-sheets-datasource/pkg/apis/googlesheets/v1.DatasourceStatus"),
// },
// },
},
},
},
Dependencies: []string{
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta",
},
}
}
func schema_pkg_PlaylistList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "PlaylistList",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref(kindKey),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
kindKey,
"k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}

View File

@@ -0,0 +1,104 @@
package v0alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
common "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
grafanarest "github.com/grafana/grafana/pkg/services/grafana-apiserver/rest"
"github.com/grafana/grafana/pkg/services/playlist"
)
// GroupName is the group name for this API.
const GroupName = "playlist.x.grafana.com"
const VersionID = "v0alpha1" //
const APIVersion = GroupName + "/" + VersionID
var _ grafanaapiserver.APIGroupBuilder = (*PlaylistAPIBuilder)(nil)
// This is used just so wire has something unique to return
type PlaylistAPIBuilder struct {
service playlist.Service
}
func RegisterAPIService(p playlist.Service, apiregistration grafanaapiserver.APIRegistrar) *PlaylistAPIBuilder {
builder := &PlaylistAPIBuilder{
service: p,
}
apiregistration.RegisterAPI(builder)
return builder
}
func (b *PlaylistAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
err := AddToScheme(scheme)
if err != nil {
return err
}
return scheme.SetVersionPriority(SchemeGroupVersion)
}
func (b *PlaylistAPIBuilder) GetAPIGroupInfo(
scheme *runtime.Scheme,
codecs serializer.CodecFactory, // pointer?
optsGetter generic.RESTOptionsGetter,
) (*genericapiserver.APIGroupInfo, error) {
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(GroupName, scheme, metav1.ParameterCodec, codecs)
storage := map[string]rest.Storage{}
legacyStore := newLegacyStorage(b.service)
storage["playlists"] = legacyStore
// enable dual writes if a RESTOptionsGetter is provided
if optsGetter != nil {
store, err := newStorage(scheme, optsGetter)
if err != nil {
return nil, err
}
storage["playlists"] = grafanarest.NewDualWriter(legacyStore, store)
}
apiGroupInfo.VersionedResourcesStorageMap[VersionID] = storage
return &apiGroupInfo, nil
}
func (b *PlaylistAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return getOpenAPIDefinitions
}
// Register additional routes with the server
func (b *PlaylistAPIBuilder) GetOpenAPIPostProcessor() func(*spec3.OpenAPI) (*spec3.OpenAPI, error) {
return nil
}
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: VersionID}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
var (
// SchemeBuilder points to a list of functions added to Scheme.
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
localSchemeBuilder = &SchemeBuilder
// AddToScheme is a common registration function for mapping packaged scoped group & version keys to a scheme.
AddToScheme = localSchemeBuilder.AddToScheme
)
// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Playlist{},
&PlaylistList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

View File

@@ -0,0 +1,40 @@
package v0alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
grafanaregistry "github.com/grafana/grafana/pkg/services/grafana-apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/services/grafana-apiserver/rest"
)
var _ grafanarest.Storage = (*storage)(nil)
type storage struct {
*genericregistry.Store
}
func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) {
strategy := grafanaregistry.NewStrategy(scheme)
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &Playlist{} },
NewListFunc: func() runtime.Object { return &PlaylistList{} },
PredicateFunc: grafanaregistry.Matcher,
DefaultQualifiedResource: Resource("playlists"),
SingularQualifiedResource: Resource("playlist"),
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
TableConvertor: rest.NewDefaultTableConvertor(Resource("playlists")),
}
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs}
if err := store.CompleteWithOptions(options); err != nil {
return nil, err
}
return &storage{Store: store}, nil
}

View File

@@ -0,0 +1,25 @@
package v0alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Playlist struct {
metav1.TypeMeta `json:",inline"`
// Standard object's metadata
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`
Name string `json:"name,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type PlaylistList struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ListMeta `json:"metadata,omitempty"`
Items []Playlist `json:"playlists,omitempty"`
}

View File

@@ -0,0 +1,85 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// Code generated by deepcopy-gen. DO NOT EDIT.
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Playlist) DeepCopyInto(out *Playlist) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Playlist.
func (in *Playlist) DeepCopy() *Playlist {
if in == nil {
return nil
}
out := new(Playlist)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Playlist) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PlaylistList) DeepCopyInto(out *PlaylistList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Playlist, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlaylistList.
func (in *PlaylistList) DeepCopy() *PlaylistList {
if in == nil {
return nil
}
out := new(PlaylistList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PlaylistList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *legacyStorage) DeepCopyInto(out *legacyStorage) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Storage.
func (in *legacyStorage) DeepCopy() *legacyStorage {
if in == nil {
return nil
}
out := new(legacyStorage)
in.DeepCopyInto(out)
return out
}