K8s: Allow more control over the final openapi results (#81829)

This commit is contained in:
Ryan McKinley 2024-02-02 14:19:45 -08:00 committed by GitHub
parent 651faff08a
commit ba3ee60711
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 156 additions and 60 deletions

View File

@ -150,7 +150,7 @@ Experimental features might be changed or removed without prior notice.
| `panelMonitoring` | Enables panel monitoring through logs and measurements |
| `enableNativeHTTPHistogram` | Enables native HTTP Histograms |
| `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s |
| `kubernetesSnapshots` | Use the kubernetes API in the frontend to support playlists |
| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint |
| `lokiStructuredMetadata` | Enables the loki data source to request structured metadata from the Loki server |
| `teamHttpHeaders` | Enables datasources to apply team headers to the client requests |
| `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. |

View File

@ -625,7 +625,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/avatar/:hash", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), hs.AvatarCacheServer.Handler)
// Snapshots
r.Post("/api/snapshots/", reqSnapshotPublicModeOrSignedIn, hs.CreateDashboardSnapshot)
r.Post("/api/snapshots/", reqSnapshotPublicModeOrSignedIn, hs.getCreatedSnapshotHandler())
r.Get("/api/snapshot/shared-options/", reqSignedIn, hs.GetSharingOptions)
r.Get("/api/snapshots/:key", routing.Wrap(hs.GetDashboardSnapshot))
r.Get("/api/snapshots-delete/:deleteKey", reqSnapshotPublicModeOrSignedIn, routing.Wrap(hs.DeleteDashboardSnapshotByDeleteKey))

View File

@ -2,21 +2,44 @@ package api
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil/errhttp"
"github.com/grafana/grafana/pkg/web"
)
// r.Post("/api/snapshots/"
func (hs *HTTPServer) getCreatedSnapshotHandler() web.Handler {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesSnapshots) {
namespaceMapper := request.GetNamespaceMapper(hs.Cfg)
return func(w http.ResponseWriter, r *http.Request) {
user, err := appcontext.User(r.Context())
if err != nil || user == nil {
errhttp.Write(r.Context(), fmt.Errorf("no user"), w)
return
}
r.URL.Path = "/apis/dashboardsnapshot.grafana.app/v0alpha1/namespaces/" +
namespaceMapper(user.OrgID) + "/dashboardsnapshots/create"
hs.clientConfigProvider.DirectlyServeHTTP(w, r)
}
}
return hs.CreateDashboardSnapshot
}
// swagger:route GET /snapshot/shared-options snapshots getSharingOptions
//
// Get snapshot sharing settings.

View File

@ -37,6 +37,7 @@ import (
)
var _ builder.APIGroupBuilder = (*SnapshotsAPIBuilder)(nil)
var _ builder.OpenAPIPostProcessor = (*SnapshotsAPIBuilder)(nil)
var resourceInfo = dashboardsnapshot.DashboardSnapshotResourceInfo
@ -183,11 +184,21 @@ func (b *SnapshotsAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
{
Path: prefix + "/create",
Spec: &spec3.PathProps{
Summary: "an example at the root level",
Description: "longer description here?",
Post: &spec3.Operation{
VendorExtensible: spec.VendorExtensible{
Extensions: map[string]any{
"x-grafana-action": "create",
"x-kubernetes-group-version-kind": metav1.GroupVersionKind{
Group: dashboardsnapshot.GROUP,
Version: dashboardsnapshot.VERSION,
Kind: "DashboardCreateResponse",
},
},
},
OperationProps: spec3.OperationProps{
Tags: tags,
Tags: tags,
Summary: "Full dashboard",
Description: "longer description here?",
Parameters: []*spec3.Parameter{
{
ParameterProps: spec3.ParameterProps{
@ -343,3 +354,24 @@ func (b *SnapshotsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return authorizer.DecisionNoOpinion, "", err
})
}
func (b *SnapshotsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
oas.Info.Description = "A dashboard snapshot shares an interactive dashboard publicly."
// Set a description on the
sub := oas.Paths.Paths["/apis/dashboardsnapshot.grafana.app/v0alpha1/namespaces/{namespace}/dashboardsnapshots/{name}/body"]
if sub != nil && sub.Get != nil {
sub.Get.Summary = "Full dashboard"
sub.Get.Description = "Read the full dashboard body"
}
// Hide the invalid endpoint to list all snapshots for all orgs
delete(oas.Paths.Paths, "/apis/dashboardsnapshot.grafana.app/v0alpha1/dashboardsnapshots")
// The root API discovery list
sub = oas.Paths.Paths["/apis/dashboardsnapshot.grafana.app/v0alpha1/"]
if sub != nil && sub.Get != nil {
sub.Get.Tags = []string{"API Discovery"} // sorts first in the list
}
return oas, nil
}

View File

@ -42,6 +42,11 @@ type APIGroupBuilder interface {
GetAuthorizer() authorizer.Authorizer
}
// Builders that implement OpenAPIPostProcessor are given a chance to modify the schema directly
type OpenAPIPostProcessor interface {
PostProcessOpenAPI(*spec3.OpenAPI) (*spec3.OpenAPI, error)
}
// This is used to implement dynamic sub-resources like pods/x/logs
type APIRouteHandler struct {
Path string // added to the appropriate level

View File

@ -2,12 +2,15 @@ package builder
import (
"maps"
"strings"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
common "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
spec "k8s.io/kube-openapi/pkg/validation/spec"
"github.com/grafana/grafana/pkg/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/setting"
)
// This should eventually live in grafana-app-sdk
@ -26,6 +29,92 @@ func GetOpenAPIDefinitions(builders []APIGroupBuilder) common.GetOpenAPIDefiniti
}
}
// 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 _, b := range builders {
routes := b.GetAPIRoutes()
gv := b.GetGroupVersion()
prefix := "/apis/" + gv.String() + "/"
if s.Paths.Paths[prefix] != nil {
copy := spec3.OpenAPI{
Version: s.Version,
Info: &spec.Info{
InfoProps: spec.InfoProps{
Title: gv.String(),
Version: setting.BuildVersion,
},
},
Components: s.Components,
ExternalDocs: s.ExternalDocs,
Servers: s.Servers,
Paths: s.Paths,
}
if routes == nil {
routes = &APIRoutes{}
}
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,
}
}
// Make the sub-resources (connect) share the same tags as the main resource
for path, spec := range copy.Paths.Paths {
idx := strings.LastIndex(path, "{name}/")
if idx > 0 {
parent := copy.Paths.Paths[path[:idx+6]]
if parent != nil && parent.Get != nil {
for _, op := range GetPathOperations(spec) {
if op != nil && op.Extensions != nil {
action, ok := op.Extensions.GetString("x-kubernetes-action")
if ok && action == "connect" {
op.Tags = parent.Get.Tags
}
}
}
}
}
}
// Support direct manipulation of API results
processor, ok := b.(OpenAPIPostProcessor)
if ok {
return processor.PostProcessOpenAPI(&copy)
}
return &copy, nil
}
}
return s, nil
}
}
func GetPathOperations(path *spec3.Path) []*spec3.Operation {
return []*spec3.Operation{
path.Get,
path.Head,
path.Delete,
path.Patch,
path.Post,
path.Put,
path.Trace,
path.Options,
}
}
func getStandardOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref),

View File

@ -7,9 +7,6 @@ import (
"github.com/gorilla/mux"
restclient "k8s.io/client-go/rest"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
"github.com/grafana/grafana/pkg/setting"
)
type requestHandler struct {
@ -116,53 +113,3 @@ 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 _, b := range builders {
routes := b.GetAPIRoutes()
gv := b.GetGroupVersion()
prefix := "/apis/" + gv.String() + "/"
if s.Paths.Paths[prefix] != nil {
copy := spec3.OpenAPI{
Version: s.Version,
Info: &spec.Info{
InfoProps: spec.InfoProps{
Title: gv.String(),
Version: setting.BuildVersion,
},
},
Components: s.Components,
ExternalDocs: s.ExternalDocs,
Servers: s.Servers,
Paths: s.Paths,
}
if routes == nil {
routes = &APIRoutes{}
}
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,
}
}
return &copy, nil
}
}
return s, nil
}
}

View File

@ -951,7 +951,7 @@ var (
},
{
Name: "kubernetesSnapshots",
Description: "Use the kubernetes API in the frontend to support playlists",
Description: "Routes snapshot requests from /api to the /apis endpoint",
Stage: FeatureStageExperimental,
Owner: grafanaAppPlatformSquad,
RequiresRestart: true, // changes the API routing

View File

@ -456,7 +456,7 @@ const (
FlagKubernetesPlaylists = "kubernetesPlaylists"
// FlagKubernetesSnapshots
// Use the kubernetes API in the frontend to support playlists
// Routes snapshot requests from /api to the /apis endpoint
FlagKubernetesSnapshots = "kubernetesSnapshots"
// FlagKubernetesQueryServiceRewrite