From 0f398e940df7f535150ae3e92b4ae2acf71f045b Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Fri, 22 Sep 2023 14:17:53 -0400 Subject: [PATCH] K8s: Playlist API example (#75260) K8s: Playlist example --- .github/CODEOWNERS | 1 + pkg/apis/install/install.go | 14 +++ pkg/apis/playlist/v1/doc.go | 5 + pkg/apis/playlist/v1/handler.go | 76 +++++++++++++++ pkg/apis/playlist/v1/register.go | 36 +++++++ pkg/apis/playlist/v1/types.go | 27 ++++++ pkg/apis/playlist/v1/zz_generated.deepcopy.go | 87 +++++++++++++++++ pkg/services/grafana-apiserver/service.go | 97 ++++++++++++------- 8 files changed, 308 insertions(+), 35 deletions(-) create mode 100644 pkg/apis/install/install.go create mode 100644 pkg/apis/playlist/v1/doc.go create mode 100644 pkg/apis/playlist/v1/handler.go create mode 100644 pkg/apis/playlist/v1/register.go create mode 100644 pkg/apis/playlist/v1/types.go create mode 100644 pkg/apis/playlist/v1/zz_generated.deepcopy.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d14843de783..ea3b6408666 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -67,6 +67,7 @@ /scripts/modowners/ @grafana/backend-platform /pkg/api/ @grafana/backend-platform +/pkg/apis/ @grafana/grafana-app-platform-squad /pkg/bus/ @grafana/backend-platform /pkg/cmd/ @grafana/backend-platform /pkg/components/apikeygen/ @grafana/grafana-authnz-team diff --git a/pkg/apis/install/install.go b/pkg/apis/install/install.go new file mode 100644 index 00000000000..6103fbd3bb2 --- /dev/null +++ b/pkg/apis/install/install.go @@ -0,0 +1,14 @@ +package install + +import ( + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + + playlistv1 "github.com/grafana/grafana/pkg/apis/playlist/v1" +) + +// Install registers the API group and adds types to a scheme +func Install(scheme *runtime.Scheme) { + utilruntime.Must(playlistv1.AddToScheme(scheme)) + utilruntime.Must(scheme.SetVersionPriority(playlistv1.SchemeGroupVersion)) +} diff --git a/pkg/apis/playlist/v1/doc.go b/pkg/apis/playlist/v1/doc.go new file mode 100644 index 00000000000..df8d6926643 --- /dev/null +++ b/pkg/apis/playlist/v1/doc.go @@ -0,0 +1,5 @@ +// +k8s:deepcopy-gen=package +// +k8s:openapi-gen=true +// +groupName=playlist.grafana.io + +package v1 // import "github.com/grafana/grafana/pkg/apis/playlist/v1" diff --git a/pkg/apis/playlist/v1/handler.go b/pkg/apis/playlist/v1/handler.go new file mode 100644 index 00000000000..f88feddbad3 --- /dev/null +++ b/pkg/apis/playlist/v1/handler.go @@ -0,0 +1,76 @@ +package v1 + +import ( + "context" + + "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" +) + +var _ rest.Scoper = (*Handler)(nil) +var _ rest.SingularNameProvider = (*Handler)(nil) +var _ rest.Getter = (*Handler)(nil) +var _ rest.Lister = (*Handler)(nil) +var _ rest.Storage = (*Handler)(nil) + +type Handler struct{} + +func (r *Handler) New() runtime.Object { + return &Playlist{} +} + +func (r *Handler) Destroy() {} + +func (r *Handler) NamespaceScoped() bool { + return true +} + +func (r *Handler) GetSingularName() string { + return "playlist" +} + +func (r *Handler) NewList() runtime.Object { + return &PlaylistList{} +} + +func (r *Handler) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return rest.NewDefaultTableConvertor(Resource("playlists")).ConvertToTable(ctx, object, tableOptions) +} + +func (r *Handler) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + // TODO: replace + return &PlaylistList{ + TypeMeta: metav1.TypeMeta{ + Kind: "PlaylistList", + APIVersion: "playlist.grafana.io/v1", + }, + Items: []Playlist{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "Playlist", + APIVersion: "playlist.grafana.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Name: "test", + }, + }, + }, nil +} + +func (r *Handler) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + // TODO: replace + return &Playlist{ + TypeMeta: metav1.TypeMeta{ + Kind: "Playlist", + APIVersion: "playlist.grafana.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Name: "test", + }, nil +} diff --git a/pkg/apis/playlist/v1/register.go b/pkg/apis/playlist/v1/register.go new file mode 100644 index 00000000000..1cf02d44116 --- /dev/null +++ b/pkg/apis/playlist/v1/register.go @@ -0,0 +1,36 @@ +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName is the group name for this API. +const GroupName = "playlist.grafana.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"} + +// 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 +} diff --git a/pkg/apis/playlist/v1/types.go b/pkg/apis/playlist/v1/types.go new file mode 100644 index 00000000000..80ba3dd76df --- /dev/null +++ b/pkg/apis/playlist/v1/types.go @@ -0,0 +1,27 @@ +package v1 + +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"` + // 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"` + + Items []Playlist `json:"playlists,omitempty"` +} diff --git a/pkg/apis/playlist/v1/zz_generated.deepcopy.go b/pkg/apis/playlist/v1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..9f4ccc8b37a --- /dev/null +++ b/pkg/apis/playlist/v1/zz_generated.deepcopy.go @@ -0,0 +1,87 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// generated by scripts/k8s/update-codegen.sh + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1 + +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.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + 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 *Handler) DeepCopyInto(out *Handler) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Storage. +func (in *Handler) DeepCopy() *Handler { + if in == nil { + return nil + } + out := new(Handler) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/services/grafana-apiserver/service.go b/pkg/services/grafana-apiserver/service.go index c7a7db00c01..119171fd9c5 100644 --- a/pkg/services/grafana-apiserver/service.go +++ b/pkg/services/grafana-apiserver/service.go @@ -4,27 +4,29 @@ import ( "context" "crypto/x509" "net" - "os" "path" "github.com/go-logr/logr" "github.com/grafana/dskit/services" - kindsv1 "github.com/grafana/grafana-apiserver/pkg/apis/kinds/v1" - grafanaapiserver "github.com/grafana/grafana-apiserver/pkg/apiserver" "github.com/grafana/grafana-apiserver/pkg/certgenerator" - grafanaapiserveroptions "github.com/grafana/grafana-apiserver/pkg/cmd/server/options" - "github.com/grafana/grafana-apiserver/pkg/storage/filepath" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/request/headerrequest" "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/options" - "k8s.io/client-go/rest" + clientrest "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/klog/v2" + "github.com/grafana/grafana/pkg/apis/install" + playlistv1 "github.com/grafana/grafana/pkg/apis/playlist/v1" "github.com/grafana/grafana/pkg/modules" ) @@ -37,18 +39,41 @@ var ( _ RestConfigProvider = (*service)(nil) ) +var ( + Scheme = runtime.NewScheme() + Codecs = serializer.NewCodecFactory(Scheme) + + // if you modify this, make sure you update the crEncoder + unversionedVersion = schema.GroupVersion{Group: "", Version: "v1"} + unversionedTypes = []runtime.Object{ + &metav1.Status{}, + &metav1.WatchEvent{}, + &metav1.APIVersions{}, + &metav1.APIGroupList{}, + &metav1.APIGroup{}, + &metav1.APIResourceList{}, + } +) + +func init() { + install.Install(Scheme) + // we need to add the options to empty v1 + metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Group: "", Version: "v1"}) + Scheme.AddUnversionedTypes(unversionedVersion, unversionedTypes...) +} + type Service interface { services.NamedService } type RestConfigProvider interface { - GetRestConfig() *rest.Config + GetRestConfig() *clientrest.Config } type service struct { *services.BasicService - restConfig *rest.Config + restConfig *clientrest.Config dataPath string stopCh chan struct{} @@ -66,7 +91,7 @@ func New(dataPath string) (*service, error) { return s, nil } -func (s *service) GetRestConfig() *rest.Config { +func (s *service) GetRestConfig() *clientrest.Config { return s.restConfig } @@ -75,15 +100,15 @@ func (s *service) start(ctx context.Context) error { logger.V(9) klog.SetLoggerWithOptions(logger, klog.ContextualLogger(true)) - o := grafanaapiserveroptions.NewGrafanaAPIServerOptions(os.Stdout, os.Stderr) - o.RecommendedOptions.SecureServing.BindPort = 6443 - o.RecommendedOptions.Authentication.RemoteKubeConfigFileOptional = true - o.RecommendedOptions.Authorization.RemoteKubeConfigFileOptional = true - o.RecommendedOptions.Authorization.AlwaysAllowPaths = []string{"*"} - o.RecommendedOptions.Authorization.AlwaysAllowGroups = []string{user.SystemPrivilegedGroup, "grafana"} - o.RecommendedOptions.Etcd = nil - o.RecommendedOptions.Admission = nil - o.RecommendedOptions.CoreAPI = nil + o := options.NewRecommendedOptions("", unstructured.UnstructuredJSONScheme) + o.SecureServing.BindPort = 6443 + o.Authentication.RemoteKubeConfigFileOptional = true + o.Authorization.RemoteKubeConfigFileOptional = true + o.Authorization.AlwaysAllowPaths = []string{"*"} + o.Authorization.AlwaysAllowGroups = []string{user.SystemPrivilegedGroup, "grafana"} + o.Etcd = nil + o.Admission = nil + o.CoreAPI = nil // Get the util to get the paths to pre-generated certs certUtil := certgenerator.CertUtil{ @@ -98,53 +123,55 @@ func (s *service) start(ctx context.Context) error { return err } - o.RecommendedOptions.SecureServing.BindAddress = net.ParseIP(certgenerator.DefaultAPIServerIp) - o.RecommendedOptions.SecureServing.ServerCert.CertKey = options.CertKey{ + o.SecureServing.BindAddress = net.ParseIP(certgenerator.DefaultAPIServerIp) + o.SecureServing.ServerCert.CertKey = options.CertKey{ CertFile: certUtil.APIServerCertFile(), KeyFile: certUtil.APIServerKeyFile(), } - if err := o.Complete(); err != nil { - return err + if err := o.Validate(); len(err) > 0 { + return err[0] } - if err := o.Validate(); err != nil { + serverConfig := genericapiserver.NewRecommendedConfig(Codecs) + err := o.ApplyTo(serverConfig) + if err != nil { return err } - serverConfig, err := o.Config() + rootCert, err := certUtil.GetK8sCACert() if err != nil { return err } - rootCert, err := certUtil.GetK8sCACert() + authenticator, err := newAuthenticator(rootCert) if err != nil { return err } - serverConfig.ExtraConfig.RESTOptionsGetter = filepath.NewRESTOptionsGetter(s.dataPath, unstructured.UnstructuredJSONScheme) - serverConfig.GenericConfig.RESTOptionsGetter = filepath.NewRESTOptionsGetter(s.dataPath, grafanaapiserver.Codecs.LegacyCodec(kindsv1.SchemeGroupVersion)) - serverConfig.GenericConfig.Config.RESTOptionsGetter = filepath.NewRESTOptionsGetter(s.dataPath, grafanaapiserver.Codecs.LegacyCodec(kindsv1.SchemeGroupVersion)) + serverConfig.Authentication.Authenticator = authenticator - authenticator, err := newAuthenticator(rootCert) + server, err := serverConfig.Complete().New("grafana-apiserver", genericapiserver.NewEmptyDelegate()) if err != nil { return err } - serverConfig.GenericConfig.Authentication.Authenticator = authenticator + apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(playlistv1.GroupName, Scheme, metav1.ParameterCodec, Codecs) + playlistv1storage := map[string]rest.Storage{} + playlistv1storage["playlists"] = &playlistv1.Handler{} - server, err := serverConfig.Complete().New(genericapiserver.NewEmptyDelegate()) - if err != nil { + apiGroupInfo.VersionedResourcesStorageMap["v1"] = playlistv1storage + if err := server.InstallAPIGroup(&apiGroupInfo); err != nil { return err } - s.restConfig = server.GenericAPIServer.LoopbackClientConfig + s.restConfig = server.LoopbackClientConfig err = s.writeKubeConfiguration(s.restConfig) if err != nil { return err } - prepared := server.GenericAPIServer.PrepareRun() + prepared := server.PrepareRun() // TODO: not sure if we can still inject RouteRegister with the new module server setup // Disabling the /k8s endpoint until we have a solution @@ -192,7 +219,7 @@ func (s *service) running(ctx context.Context) error { return nil } -func (s *service) writeKubeConfiguration(restConfig *rest.Config) error { +func (s *service) writeKubeConfiguration(restConfig *clientrest.Config) error { clusters := make(map[string]*clientcmdapi.Cluster) clusters["default-cluster"] = &clientcmdapi.Cluster{ Server: restConfig.Host,