K8s/Playlist: Isolate apiGroup from server (#75321)

This commit is contained in:
Ryan McKinley 2023-09-22 18:29:43 -07:00 committed by GitHub
parent 042833376a
commit e72b5c54f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 2818 additions and 47 deletions

63
pkg/apis/common.go Normal file
View File

@ -0,0 +1,63 @@
package apis
import (
"fmt"
"strconv"
"strings"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
)
// TODO: this (or something like it) belongs in grafana-app-sdk,
// but lets keep it here while we iterate on a few simple examples
type APIGroupBuilder interface {
// Add the kinds to the server scheme
InstallSchema(scheme *runtime.Scheme) error
// Build the group+version behavior
GetAPIGroupInfo(
scheme *runtime.Scheme,
codecs serializer.CodecFactory, // pointer?
) *genericapiserver.APIGroupInfo
// Get OpenAPI definitions
GetOpenAPIDefinitions() common.GetOpenAPIDefinitions
// Register additional routes with the server
GetOpenAPIPostProcessor() func(*spec3.OpenAPI) (*spec3.OpenAPI, error)
}
func OrgIdToNamespace(orgId int64) string {
if orgId > 1 {
return fmt.Sprintf("org-%d", orgId)
}
return "default"
}
func NamespaceToOrgID(ns string) (int64, error) {
parts := strings.Split(ns, "-")
switch len(parts) {
case 1:
if parts[0] == "default" {
return 1, nil
}
if parts[0] == "" {
return 0, nil // no orgId, cluster scope
}
return 0, fmt.Errorf("invalid namespace (expected default)")
case 2:
if !(parts[0] == "org" || parts[0] == "tenant") {
return 0, fmt.Errorf("invalid namespace (org|tenant)")
}
n, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid namepscae (%w)", err)
}
return n, nil
}
return 0, fmt.Errorf("invalid namespace (%d parts)", len(parts))
}

View File

@ -1,14 +0,0 @@
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))
}

View File

@ -2,55 +2,70 @@ package v1
import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/apis"
"github.com/grafana/grafana/pkg/services/playlist"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/endpoints/request"
"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)
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{}
type handler struct {
service playlist.Service
}
func (r *Handler) New() runtime.Object {
func (r *handler) New() runtime.Object {
return &Playlist{}
}
func (r *Handler) Destroy() {}
func (r *handler) Destroy() {}
func (r *Handler) NamespaceScoped() bool {
return true
func (r *handler) NamespaceScoped() bool {
return true // namespace == org
}
func (r *Handler) GetSingularName() string {
func (r *handler) GetSingularName() string {
return "playlist"
}
func (r *Handler) NewList() runtime.Object {
func (r *handler) NewList() runtime.Object {
return &PlaylistList{}
}
func (r *Handler) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
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) {
func (r *handler) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
ns, ok := request.NamespaceFrom(ctx)
if ok && ns != "" {
orgId, err := apis.NamespaceToOrgID(ns)
if err != nil {
return nil, err
}
fmt.Printf("OrgID: %d\n", orgId)
}
// TODO: replace
return &PlaylistList{
TypeMeta: metav1.TypeMeta{
Kind: "PlaylistList",
APIVersion: "playlist.grafana.io/v1",
APIVersion: APIVersion,
},
Items: []Playlist{
{
TypeMeta: metav1.TypeMeta{
Kind: "Playlist",
APIVersion: "playlist.grafana.io/v1",
APIVersion: APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
@ -61,12 +76,21 @@ func (r *Handler) List(ctx context.Context, options *internalversion.ListOptions
}, nil
}
func (r *Handler) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
func (r *handler) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
ns, ok := request.NamespaceFrom(ctx)
if ok && ns != "" {
orgId, err := apis.NamespaceToOrgID(ns)
if err != nil {
return nil, err
}
fmt.Printf("OrgID: %d\n", orgId)
}
// TODO: replace
return &Playlist{
TypeMeta: metav1.TypeMeta{
Kind: "Playlist",
APIVersion: "playlist.grafana.io/v1",
APIVersion: APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,

View File

@ -0,0 +1,115 @@
package v1
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/v1.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

@ -1,16 +1,62 @@
package v1
import (
"github.com/grafana/grafana/pkg/apis"
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/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
common "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
)
// GroupName is the group name for this API.
const GroupName = "playlist.grafana.io"
const GroupName = "playlist.x.grafana.com"
const VersionID = "v0-alpha" //
const APIVersion = GroupName + "/" + VersionID
type builder struct{}
// TODO.. this will have wire dependencies
func GetAPIGroupBuilder() apis.APIGroupBuilder {
return &builder{}
}
func (b *builder) InstallSchema(scheme *runtime.Scheme) error {
err := AddToScheme(scheme)
if err != nil {
return err
}
return scheme.SetVersionPriority(SchemeGroupVersion)
}
func (b *builder) GetAPIGroupInfo(
scheme *runtime.Scheme,
codecs serializer.CodecFactory, // pointer?
) *genericapiserver.APIGroupInfo {
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(GroupName, scheme, metav1.ParameterCodec, codecs)
storage := map[string]rest.Storage{}
storage["playlists"] = &handler{
service: nil, // TODO!!!!!
}
apiGroupInfo.VersionedResourcesStorageMap[VersionID] = storage
return &apiGroupInfo
}
func (b *builder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return getOpenAPIDefinitions
}
// Register additional routes with the server
func (b *builder) 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: "v1"}
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 {

View File

@ -71,17 +71,17 @@ func (in *PlaylistList) DeepCopyObject() runtime.Object {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Handler) DeepCopyInto(out *Handler) {
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 {
func (in *handler) DeepCopy() *handler {
if in == nil {
return nil
}
out := new(Handler)
out := new(handler)
in.DeepCopyInto(out)
return out
}

File diff suppressed because it is too large Load Diff

View File

@ -17,15 +17,17 @@ import (
"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"
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/pkg/util/openapi"
"k8s.io/client-go/kubernetes/scheme"
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"
"github.com/grafana/grafana/pkg/apis"
playlistv1 "github.com/grafana/grafana/pkg/apis/playlist/v1"
"github.com/grafana/grafana/pkg/modules"
)
@ -56,7 +58,6 @@ var (
)
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...)
@ -151,18 +152,43 @@ func (s *service) start(ctx context.Context) error {
serverConfig.Authentication.Authenticator = authenticator
// Get the list of groups the server will support
builders := []apis.APIGroupBuilder{
playlistv1.GetAPIGroupBuilder(),
}
// Install schemas
for _, b := range builders {
err = b.InstallSchema(Scheme) // previously was in init
if err != nil {
return err
}
}
// Add OpenAPI specs for each group+version
defsGetter := getOpenAPIDefinitions(builders)
serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(
openapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(defsGetter),
openapinamer.NewDefinitionNamer(Scheme, scheme.Scheme))
serverConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(
openapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(defsGetter),
openapinamer.NewDefinitionNamer(Scheme, scheme.Scheme))
serverConfig.SkipOpenAPIInstallation = false
// Create the server
server, err := serverConfig.Complete().New("grafana-apiserver", genericapiserver.NewEmptyDelegate())
if err != nil {
return err
}
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(playlistv1.GroupName, Scheme, metav1.ParameterCodec, Codecs)
playlistv1storage := map[string]rest.Storage{}
playlistv1storage["playlists"] = &playlistv1.Handler{}
apiGroupInfo.VersionedResourcesStorageMap["v1"] = playlistv1storage
if err := server.InstallAPIGroup(&apiGroupInfo); err != nil {
return err
// Install the API Group+version
for _, b := range builders {
err = server.InstallAPIGroup(b.GetAPIGroupInfo(Scheme, Codecs))
if err != nil {
return err
}
}
s.restConfig = server.LoopbackClientConfig