K8s: Add example api service (#75911)

This commit is contained in:
Ryan McKinley
2023-10-06 11:55:22 -07:00
committed by GitHub
parent 0a50ca7231
commit 717a9dd616
19 changed files with 702 additions and 11 deletions

View File

@@ -1,7 +1,10 @@
package grafanaapiserver
import (
"net/http"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/registry/generic"
genericapiserver "k8s.io/apiserver/pkg/server"
@@ -12,6 +15,9 @@ import (
// 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 {
// Get the main group name
GetGroupVersion() schema.GroupVersion
// Add the kinds to the server scheme
InstallSchema(scheme *runtime.Scheme) error
@@ -25,6 +31,27 @@ type APIGroupBuilder interface {
// Get OpenAPI definitions
GetOpenAPIDefinitions() common.GetOpenAPIDefinitions
// Register additional routes with the server
GetOpenAPIPostProcessor() func(*spec3.OpenAPI) (*spec3.OpenAPI, error)
// Get the API routes for each version
GetAPIRoutes() *APIRoutes
}
// This is used to implement dynamic sub-resources like pods/x/logs
type APIRouteHandler struct {
Path string // added to the appropriate level
Spec *spec3.PathProps // Exposed in the open api service discovery
Handler http.HandlerFunc // when Level = resource, the resource will be available in context
}
// APIRoutes define the
type APIRoutes struct {
// Root handlers are registered directly after the apiVersion identifier
Root []APIRouteHandler
// Namespace handlers are mounted under the namespace
Namespace []APIRouteHandler
// Resource routes behave the same as pod/logs
// it looks like a sub-resource, however the response is backed directly by an http handler
// The current resource can be fetched through context
Resource map[string]APIRouteHandler
}

View File

@@ -0,0 +1,198 @@
package grafanaapiserver
import (
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
restclient "k8s.io/client-go/rest"
"k8s.io/kube-openapi/pkg/spec3"
)
type requestHandler struct {
router *mux.Router
}
func getAPIHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder) (http.Handler, error) {
useful := false // only true if any routes exist anywhere
router := mux.NewRouter()
var err error
for _, builder := range builders {
routes := builder.GetAPIRoutes()
if routes == nil {
continue
}
gv := builder.GetGroupVersion()
prefix := "/apis/" + gv.String()
// Root handlers
var sub *mux.Router
for _, route := range routes.Root {
err = validPath(route.Path)
if err != nil {
return nil, err
}
if sub == nil {
sub = router.PathPrefix(prefix).Subrouter()
sub.MethodNotAllowedHandler = &methodNotAllowedHandler{}
}
useful = true
methods, err := methodsFromSpec(route.Path, route.Spec)
if err != nil {
return nil, err
}
sub.HandleFunc(route.Path, route.Handler).
Methods(methods...)
}
// Namespace handlers
sub = nil
prefix += "/namespaces/{namespace}"
for _, route := range routes.Namespace {
err = validPath(route.Path)
if err != nil {
return nil, err
}
if sub == nil {
sub = router.PathPrefix(prefix).Subrouter()
sub.MethodNotAllowedHandler = &methodNotAllowedHandler{}
}
useful = true
methods, err := methodsFromSpec(route.Path, route.Spec)
if err != nil {
return nil, err
}
sub.HandleFunc(route.Path, route.Handler).
Methods(methods...)
}
// getter := makeGetter(restConfig)
for resource, route := range routes.Resource {
err = validPath(route.Path)
if err != nil {
return nil, err
}
fmt.Printf("TODO: %s/%v\n", resource, route)
// get a client for that resource kind
//getter := makeGetter(restConfig)
useful = true
// sub.HandleFunc(v.Slug, SubresourceHandlerWrapper(v.Handler, getter)).
// Methods(methods...)
}
}
if !useful {
return delegateHandler, nil
}
// Per Gorilla Mux issue here: https://github.com/gorilla/mux/issues/616#issuecomment-798807509
// default handler must come last
router.PathPrefix("/").Handler(delegateHandler)
return &requestHandler{
router: router,
}, nil
}
// The registered path must start with a slash, and (for now) not have any more
func validPath(p string) error {
if !strings.HasPrefix(p, "/") {
return fmt.Errorf("path must start with slash")
}
if strings.Count(p, "/") > 1 {
return fmt.Errorf("path can only have one slash (for now)")
}
return nil
}
func (h *requestHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
h.router.ServeHTTP(w, req)
}
func methodsFromSpec(slug string, props *spec3.PathProps) ([]string, error) {
if props == nil {
return []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, nil
}
methods := make([]string, 0)
if props.Get != nil {
methods = append(methods, "GET")
}
if props.Post != nil {
methods = append(methods, "POST")
}
if props.Put != nil {
methods = append(methods, "PUT")
}
if props.Patch != nil {
methods = append(methods, "PATCH")
}
if props.Delete != nil {
methods = append(methods, "DELETE")
}
if len(methods) == 0 {
return nil, fmt.Errorf("invalid OpenAPI Spec for slug=%s without any methods in PathProps", slug)
}
return methods, nil
}
type methodNotAllowedHandler struct{}
func (h *methodNotAllowedHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(405) // method not allowed
}
// Modify the the OpenAPI spec to include the additional routes.
// Currently this requires: https://github.com/kubernetes/kube-openapi/pull/420
// In future k8s release, the hook will use Config3 rather than the same hook for both v2 and v3
func getOpenAPIPostProcessor(builders []APIGroupBuilder) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) {
return func(s *spec3.OpenAPI) (*spec3.OpenAPI, error) {
if s.Paths == nil {
return s, nil
}
for _, builder := range builders {
routes := builder.GetAPIRoutes()
if routes == nil {
continue
}
gv := builder.GetGroupVersion()
prefix := "/apis/" + gv.String()
if s.Paths.Paths[prefix] != nil {
copy := *s // will copy the rest of the properties
copy.Info.Title = "Grafana API server: " + gv.Group
for _, route := range routes.Root {
copy.Paths.Paths[prefix+route.Path] = &spec3.Path{
PathProps: *route.Spec,
}
}
for _, route := range routes.Namespace {
copy.Paths.Paths[prefix+"/namespaces/{namespace}"+route.Path] = &spec3.Path{
PathProps: *route.Spec,
}
}
for resource, route := range routes.Resource {
copy.Paths.Paths[prefix+"/namespaces/{namespace}/"+resource+"{name}"+route.Path] = &spec3.Path{
PathProps: *route.Spec,
}
}
return &copy, nil
}
}
return s, nil
}
}

View File

@@ -3,7 +3,9 @@ package grafanaapiserver
import (
"context"
"crypto/x509"
"fmt"
"net"
"net/http"
"path"
"strconv"
@@ -248,7 +250,23 @@ func (s *service) start(ctx context.Context) error {
openapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(defsGetter),
openapinamer.NewDefinitionNamer(Scheme, scheme.Scheme))
// Add the custom routes to service discovery
serverConfig.OpenAPIV3Config.PostProcessSpec3 = getOpenAPIPostProcessor(builders)
serverConfig.SkipOpenAPIInstallation = false
serverConfig.BuildHandlerChainFunc = func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler {
// Call DefaultBuildHandlerChain on the main entrypoint http.Handler
// See https://github.com/kubernetes/apiserver/blob/v0.28.0/pkg/server/config.go#L906
// DefaultBuildHandlerChain provides many things, notably CORS, HSTS, cache-control, authz and latency tracking
requestHandler, err := getAPIHandler(
delegateHandler,
c.LoopbackClientConfig,
builders)
if err != nil {
panic(fmt.Sprintf("could not build handler chain func: %s", err.Error()))
}
return genericapiserver.DefaultBuildHandlerChain(requestHandler, c)
}
// Create the server
server, err := serverConfig.Complete().New("grafana-apiserver", genericapiserver.NewEmptyDelegate())