Compare commits

...

1 Commits

Author SHA1 Message Date
Andreas Christou
cb9246b6bd Support Schema endpoint in core 2025-12-11 18:16:46 +00:00
12 changed files with 165 additions and 0 deletions

View File

@@ -451,6 +451,7 @@ func (hs *HTTPServer) registerRoutes() {
// Deprecated: use /datasources/uid/:uid/health API instead. // Deprecated: use /datasources/uid/:uid/health API instead.
apiRoute.Any("/datasources/:id/health", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), routing.Wrap(hs.CheckDatasourceHealth)) apiRoute.Any("/datasources/:id/health", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), routing.Wrap(hs.CheckDatasourceHealth))
apiRoute.Any("/datasources/uid/:uid/health", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), routing.Wrap(hs.CheckDatasourceHealthWithUID)) apiRoute.Any("/datasources/uid/:uid/health", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), routing.Wrap(hs.CheckDatasourceHealthWithUID))
apiRoute.Any("/datasources/uid/:uid/schema", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), routing.Wrap(hs.GetDatasourceSchemaWithUID))
// Folders // Folders
hs.registerFolderAPI(apiRoute, authorize) hs.registerFolderAPI(apiRoute, authorize)

View File

@@ -775,6 +775,32 @@ func (hs *HTTPServer) convertModelToDtos(ctx context.Context, ds *datasources.Da
return dto return dto
} }
// swagger:route GET /datasources/uid/{uid}/schema datasources schema getDatasourceSchemaWithUID
//
// Sends a schema request to the plugin datasource identified by the UID.
//
// Responses:
// 200: okResponse
// 400: badRequestError
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) GetDatasourceSchemaWithUID(c *contextmodel.ReqContext) response.Response {
dsUID := web.Params(c.Req)[":uid"]
if !util.IsValidShortUID(dsUID) {
return response.Error(http.StatusBadRequest, "UID is invalid", nil)
}
ds, err := hs.DataSourceCache.GetDatasourceByUID(c.Req.Context(), dsUID, c.SignedInUser, c.SkipDSCache)
if err != nil {
if errors.Is(err, datasources.ErrDataSourceAccessDenied) {
return response.Error(http.StatusForbidden, "Access denied to datasource", err)
}
return response.Error(http.StatusInternalServerError, "Unable to load datasource metadata", err)
}
return hs.getDatasourceSchema(c, ds)
}
// swagger:route GET /datasources/uid/{uid}/health datasources health checkDatasourceHealthWithUID // swagger:route GET /datasources/uid/{uid}/health datasources health checkDatasourceHealthWithUID
// //
// Sends a health check request to the plugin datasource identified by the UID. // Sends a health check request to the plugin datasource identified by the UID.
@@ -831,6 +857,29 @@ func (hs *HTTPServer) CheckDatasourceHealth(c *contextmodel.ReqContext) response
return hs.checkDatasourceHealth(c, ds) return hs.checkDatasourceHealth(c, ds)
} }
func (hs *HTTPServer) getDatasourceSchema(c *contextmodel.ReqContext, ds *datasources.DataSource) response.Response {
pCtx, err := hs.pluginContextProvider.GetWithDataSource(c.Req.Context(), ds.Type, c.SignedInUser, ds)
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Unable to get plugin context", err)
}
req := &backend.SchemaRequest{
PluginContext: pCtx,
Headers: map[string]string{},
}
err = hs.DataSourceRequestValidator.Validate(ds, c.Req)
if err != nil {
return response.Error(http.StatusForbidden, "Access denied", err)
}
resp, err := hs.pluginClient.Schema(c.Req.Context(), req)
if err != nil {
return translatePluginRequestErrorToAPIError(err)
}
return response.JSON(http.StatusOK, resp)
}
func (hs *HTTPServer) checkDatasourceHealth(c *contextmodel.ReqContext, ds *datasources.DataSource) response.Response { func (hs *HTTPServer) checkDatasourceHealth(c *contextmodel.ReqContext, ds *datasources.DataSource) response.Response {
pCtx, err := hs.pluginContextProvider.GetWithDataSource(c.Req.Context(), ds.Type, c.SignedInUser, ds) pCtx, err := hs.pluginContextProvider.GetWithDataSource(c.Req.Context(), ds.Type, c.SignedInUser, ds)
if err != nil { if err != nil {

View File

@@ -21,6 +21,7 @@ type corePlugin struct {
backend.StreamHandler backend.StreamHandler
backend.AdmissionHandler backend.AdmissionHandler
backend.ConversionHandler backend.ConversionHandler
backend.SchemaHandler
} }
// New returns a new backendplugin.PluginFactoryFunc for creating a core (built-in) backendplugin.Plugin. // New returns a new backendplugin.PluginFactoryFunc for creating a core (built-in) backendplugin.Plugin.
@@ -152,3 +153,12 @@ func (cp *corePlugin) ConvertObjects(ctx context.Context, req *backend.Conversio
} }
return nil, plugins.ErrMethodNotImplemented return nil, plugins.ErrMethodNotImplemented
} }
func (cp *corePlugin) Schema(ctx context.Context, req *backend.SchemaRequest) (*backend.SchemaResponse, error) {
if cp.SchemaHandler != nil {
ctx = backend.WithGrafanaConfig(ctx, req.PluginContext.GrafanaConfig)
return cp.SchemaHandler.Schema(ctx, req)
}
return nil, plugins.ErrMethodNotImplemented
}

View File

@@ -41,6 +41,7 @@ var pluginSet = map[int]goplugin.PluginSet{
"admission": &grpcplugin.AdmissionGRPCPlugin{}, "admission": &grpcplugin.AdmissionGRPCPlugin{},
"conversion": &grpcplugin.ConversionGRPCPlugin{}, "conversion": &grpcplugin.ConversionGRPCPlugin{},
"renderer": &pluginextensionv2.RendererGRPCPlugin{}, "renderer": &pluginextensionv2.RendererGRPCPlugin{},
"information": &grpcplugin.InformationGRPCPlugin{},
}, },
} }

View File

@@ -189,3 +189,11 @@ func (r *protoClient) ConvertObjects(ctx context.Context, in *pluginv2.Conversio
} }
return c.ConversionClient.ConvertObjects(ctx, in, opts...) return c.ConversionClient.ConvertObjects(ctx, in, opts...)
} }
func (r *protoClient) Schema(ctx context.Context, in *pluginv2.SchemaRequest, opts ...grpc.CallOption) (*pluginv2.SchemaResponse, error) {
c, exists := r.client(ctx)
if !exists {
return nil, errClientNotAvailable
}
return c.InformationClient.Schema(ctx, in, opts...)
}

View File

@@ -30,6 +30,7 @@ type ClientV2 struct {
grpcplugin.StreamClient grpcplugin.StreamClient
grpcplugin.AdmissionClient grpcplugin.AdmissionClient
grpcplugin.ConversionClient grpcplugin.ConversionClient
grpcplugin.InformationClient
pluginextensionv2.RendererPlugin pluginextensionv2.RendererPlugin
} }
@@ -69,6 +70,11 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
return nil, err return nil, err
} }
rawInformation, err := rpcClient.Dispense("information")
if err != nil {
return nil, err
}
c := &ClientV2{} c := &ClientV2{}
if rawDiagnostics != nil { if rawDiagnostics != nil {
if diagnosticsClient, ok := rawDiagnostics.(grpcplugin.DiagnosticsClient); ok { if diagnosticsClient, ok := rawDiagnostics.(grpcplugin.DiagnosticsClient); ok {
@@ -112,6 +118,12 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
} }
} }
if rawInformation != nil {
if informationClient, ok := rawInformation.(grpcplugin.InformationClient); ok {
c.InformationClient = informationClient
}
}
if descriptor.startRendererFn != nil { if descriptor.startRendererFn != nil {
if err := descriptor.startRendererFn(descriptor.pluginID, c.RendererPlugin, logger); err != nil { if err := descriptor.startRendererFn(descriptor.pluginID, c.RendererPlugin, logger); err != nil {
return nil, err return nil, err
@@ -336,6 +348,36 @@ func (c *ClientV2) ConvertObjects(ctx context.Context, req *backend.ConversionRe
return backend.FromProto().ConversionResponse(protoResp), nil return backend.FromProto().ConversionResponse(protoResp), nil
} }
func (c *ClientV2) Schema(ctx context.Context, req *backend.SchemaRequest) (*backend.SchemaResponse, error) {
if c.InformationClient == nil {
return nil, plugins.ErrMethodNotImplemented
}
protoReq := backend.ToProto().SchemaRequest(req)
protoResp, err := c.InformationClient.Schema(ctx, protoReq)
if err != nil {
if status.Code(err) == codes.Unimplemented {
return nil, plugins.ErrMethodNotImplemented
}
if status.Code(err) == codes.Unavailable {
return nil, plugins.ErrPluginGrpcConnectionUnavailableBaseFn(ctx).Errorf("%v", err)
}
if status.Code(err) == codes.ResourceExhausted {
return nil, plugins.ErrPluginGrpcResourceExhaustedBase.Errorf("%v", err)
}
if errorSource, ok := backend.ErrorSourceFromGrpcStatusError(ctx, err); ok {
return nil, handleGrpcStatusError(ctx, errorSource, err)
}
return nil, fmt.Errorf("%v: %w", "Failed to request schema", err)
}
return backend.FromProto().SchemaResponse(protoResp), nil
}
// handleGrpcStatusError sets the error source via context based on the error source provided. Regardless of its value, // handleGrpcStatusError sets the error source via context based on the error source provided. Regardless of its value,
// a plugin downstream error is returned as both plugin and downstream errors are treated the same in Grafana. // a plugin downstream error is returned as both plugin and downstream errors are treated the same in Grafana.
func handleGrpcStatusError(ctx context.Context, errorSource errstatus.Source, err error) error { func handleGrpcStatusError(ctx context.Context, errorSource errstatus.Source, err error) error {

View File

@@ -252,3 +252,11 @@ func (p *grpcPlugin) ConvertObjects(ctx context.Context, request *backend.Conver
} }
return pc.ConvertObjects(ctx, request) return pc.ConvertObjects(ctx, request)
} }
func (p *grpcPlugin) Schema(ctx context.Context, req *backend.SchemaRequest) (*backend.SchemaResponse, error) {
pc, ok := p.getPluginClient(ctx)
if !ok {
return nil, plugins.ErrPluginUnavailable
}
return pc.Schema(ctx, req)
}

View File

@@ -26,6 +26,7 @@ type Plugin interface {
backend.AdmissionHandler backend.AdmissionHandler
backend.ConversionHandler backend.ConversionHandler
backend.StreamHandler backend.StreamHandler
backend.SchemaHandler
} }
type Target string type Target string

View File

@@ -253,6 +253,36 @@ func (s *Service) ConvertObjects(ctx context.Context, req *backend.ConversionReq
return plugin.ConvertObjects(ctx, req) return plugin.ConvertObjects(ctx, req)
} }
func (s *Service) Schema(ctx context.Context, req *backend.SchemaRequest) (*backend.SchemaResponse, error) {
if req == nil {
return nil, errNilRequest
}
p, exists := s.plugin(ctx, req.PluginContext.PluginID, req.PluginContext.PluginVersion)
if !exists {
return nil, plugins.ErrPluginNotRegistered
}
resp, err := p.Schema(ctx, req)
if err != nil {
if errors.Is(err, plugins.ErrMethodNotImplemented) {
return nil, err
}
if errors.Is(err, plugins.ErrPluginUnavailable) {
return nil, err
}
if errors.Is(err, context.Canceled) {
return nil, plugins.ErrPluginRequestCanceledErrorBase.Errorf("client: schema request canceled: %w", err)
}
return nil, plugins.ErrPluginRequestFailureErrorBase.Errorf("client: failed to request schema: %w", err)
}
return resp, nil
}
// MutateAdmission implements plugins.Client. // MutateAdmission implements plugins.Client.
func (s *Service) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { func (s *Service) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
if req == nil { if req == nil {

View File

@@ -76,6 +76,7 @@ var (
_ = backend.StreamHandler(&Plugin{}) _ = backend.StreamHandler(&Plugin{})
_ = backend.AdmissionHandler(&Plugin{}) _ = backend.AdmissionHandler(&Plugin{})
_ = backend.ConversionHandler(&Plugin{}) _ = backend.ConversionHandler(&Plugin{})
_ = backend.SchemaHandler(&Plugin{})
) )
type AngularMeta struct { type AngularMeta struct {
@@ -413,6 +414,14 @@ func (p *Plugin) ConvertObjects(ctx context.Context, req *backend.ConversionRequ
return pluginClient.ConvertObjects(ctx, req) return pluginClient.ConvertObjects(ctx, req)
} }
func (p *Plugin) Schema(ctx context.Context, req *backend.SchemaRequest) (*backend.SchemaResponse, error) {
pluginClient, ok := p.Client()
if !ok {
return nil, ErrPluginUnavailable
}
return pluginClient.Schema(ctx, req)
}
func (p *Plugin) File(name string) (fs.File, error) { func (p *Plugin) File(name string) (fs.File, error) {
cleanPath, err := util.CleanRelativePath(name) cleanPath, err := util.CleanRelativePath(name)
if err != nil { if err != nil {
@@ -470,6 +479,7 @@ type PluginClient interface {
backend.AdmissionHandler backend.AdmissionHandler
backend.ConversionHandler backend.ConversionHandler
backend.StreamHandler backend.StreamHandler
backend.SchemaHandler
} }
func (p *Plugin) StaticRoute() *StaticRoute { func (p *Plugin) StaticRoute() *StaticRoute {

View File

@@ -401,6 +401,10 @@ func (m *alwaysErrorFuncMiddleware) ConvertObjects(ctx context.Context, req *bac
return nil, m.f() return nil, m.f()
} }
func (m *alwaysErrorFuncMiddleware) Schema(ctx context.Context, req *backend.SchemaRequest) (*backend.SchemaResponse, error) {
return nil, m.f()
}
// newAlwaysErrorMiddleware returns a new middleware that always returns the specified error. // newAlwaysErrorMiddleware returns a new middleware that always returns the specified error.
func newAlwaysErrorMiddleware(err error) backend.HandlerMiddleware { func newAlwaysErrorMiddleware(err error) backend.HandlerMiddleware {
return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler {

View File

@@ -849,6 +849,7 @@ type testPlugin struct {
backend.StreamHandler backend.StreamHandler
backend.AdmissionHandler backend.AdmissionHandler
backend.ConversionHandler backend.ConversionHandler
backend.SchemaHandler
} }
func (tp *testPlugin) PluginID() string { func (tp *testPlugin) PluginID() string {