mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s: Allow more control over the final openapi results (#81829)
This commit is contained in:
parent
651faff08a
commit
ba3ee60711
@ -150,7 +150,7 @@ Experimental features might be changed or removed without prior notice.
|
|||||||
| `panelMonitoring` | Enables panel monitoring through logs and measurements |
|
| `panelMonitoring` | Enables panel monitoring through logs and measurements |
|
||||||
| `enableNativeHTTPHistogram` | Enables native HTTP Histograms |
|
| `enableNativeHTTPHistogram` | Enables native HTTP Histograms |
|
||||||
| `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s |
|
| `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 |
|
| `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 |
|
| `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. |
|
| `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. |
|
||||||
|
@ -625,7 +625,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
r.Get("/avatar/:hash", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), hs.AvatarCacheServer.Handler)
|
r.Get("/avatar/:hash", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), hs.AvatarCacheServer.Handler)
|
||||||
|
|
||||||
// Snapshots
|
// 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/snapshot/shared-options/", reqSignedIn, hs.GetSharingOptions)
|
||||||
r.Get("/api/snapshots/:key", routing.Wrap(hs.GetDashboardSnapshot))
|
r.Get("/api/snapshots/:key", routing.Wrap(hs.GetDashboardSnapshot))
|
||||||
r.Get("/api/snapshots-delete/:deleteKey", reqSnapshotPublicModeOrSignedIn, routing.Wrap(hs.DeleteDashboardSnapshotByDeleteKey))
|
r.Get("/api/snapshots-delete/:deleteKey", reqSnapshotPublicModeOrSignedIn, routing.Wrap(hs.DeleteDashboardSnapshotByDeleteKey))
|
||||||
|
@ -2,21 +2,44 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
|
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/infra/metrics"
|
||||||
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
"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/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil/errhttp"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"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
|
// swagger:route GET /snapshot/shared-options snapshots getSharingOptions
|
||||||
//
|
//
|
||||||
// Get snapshot sharing settings.
|
// Get snapshot sharing settings.
|
||||||
|
@ -37,6 +37,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var _ builder.APIGroupBuilder = (*SnapshotsAPIBuilder)(nil)
|
var _ builder.APIGroupBuilder = (*SnapshotsAPIBuilder)(nil)
|
||||||
|
var _ builder.OpenAPIPostProcessor = (*SnapshotsAPIBuilder)(nil)
|
||||||
|
|
||||||
var resourceInfo = dashboardsnapshot.DashboardSnapshotResourceInfo
|
var resourceInfo = dashboardsnapshot.DashboardSnapshotResourceInfo
|
||||||
|
|
||||||
@ -183,11 +184,21 @@ func (b *SnapshotsAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
|
|||||||
{
|
{
|
||||||
Path: prefix + "/create",
|
Path: prefix + "/create",
|
||||||
Spec: &spec3.PathProps{
|
Spec: &spec3.PathProps{
|
||||||
Summary: "an example at the root level",
|
|
||||||
Description: "longer description here?",
|
|
||||||
Post: &spec3.Operation{
|
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{
|
OperationProps: spec3.OperationProps{
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
|
Summary: "Full dashboard",
|
||||||
|
Description: "longer description here?",
|
||||||
Parameters: []*spec3.Parameter{
|
Parameters: []*spec3.Parameter{
|
||||||
{
|
{
|
||||||
ParameterProps: spec3.ParameterProps{
|
ParameterProps: spec3.ParameterProps{
|
||||||
@ -343,3 +354,24 @@ func (b *SnapshotsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
|||||||
return authorizer.DecisionNoOpinion, "", err
|
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
|
||||||
|
}
|
||||||
|
@ -42,6 +42,11 @@ type APIGroupBuilder interface {
|
|||||||
GetAuthorizer() authorizer.Authorizer
|
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
|
// This is used to implement dynamic sub-resources like pods/x/logs
|
||||||
type APIRouteHandler struct {
|
type APIRouteHandler struct {
|
||||||
Path string // added to the appropriate level
|
Path string // added to the appropriate level
|
||||||
|
@ -2,12 +2,15 @@ package builder
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
|
"strings"
|
||||||
|
|
||||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
common "k8s.io/kube-openapi/pkg/common"
|
common "k8s.io/kube-openapi/pkg/common"
|
||||||
|
"k8s.io/kube-openapi/pkg/spec3"
|
||||||
spec "k8s.io/kube-openapi/pkg/validation/spec"
|
spec "k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apis/common/v0alpha1"
|
"github.com/grafana/grafana/pkg/apis/common/v0alpha1"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This should eventually live in grafana-app-sdk
|
// 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(©)
|
||||||
|
}
|
||||||
|
return ©, 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 {
|
func getStandardOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||||
return map[string]common.OpenAPIDefinition{
|
return map[string]common.OpenAPIDefinition{
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref),
|
||||||
|
@ -7,9 +7,6 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
"k8s.io/kube-openapi/pkg/spec3"
|
"k8s.io/kube-openapi/pkg/spec3"
|
||||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type requestHandler struct {
|
type requestHandler struct {
|
||||||
@ -116,53 +113,3 @@ type methodNotAllowedHandler struct{}
|
|||||||
func (h *methodNotAllowedHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (h *methodNotAllowedHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
w.WriteHeader(405) // method not allowed
|
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 ©, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -951,7 +951,7 @@ var (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "kubernetesSnapshots",
|
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,
|
Stage: FeatureStageExperimental,
|
||||||
Owner: grafanaAppPlatformSquad,
|
Owner: grafanaAppPlatformSquad,
|
||||||
RequiresRestart: true, // changes the API routing
|
RequiresRestart: true, // changes the API routing
|
||||||
|
@ -456,7 +456,7 @@ const (
|
|||||||
FlagKubernetesPlaylists = "kubernetesPlaylists"
|
FlagKubernetesPlaylists = "kubernetesPlaylists"
|
||||||
|
|
||||||
// FlagKubernetesSnapshots
|
// FlagKubernetesSnapshots
|
||||||
// Use the kubernetes API in the frontend to support playlists
|
// Routes snapshot requests from /api to the /apis endpoint
|
||||||
FlagKubernetesSnapshots = "kubernetesSnapshots"
|
FlagKubernetesSnapshots = "kubernetesSnapshots"
|
||||||
|
|
||||||
// FlagKubernetesQueryServiceRewrite
|
// FlagKubernetesQueryServiceRewrite
|
||||||
|
Loading…
Reference in New Issue
Block a user