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.
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/schema", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), routing.Wrap(hs.GetDatasourceSchemaWithUID))
// Folders
hs.registerFolderAPI(apiRoute, authorize)

View File

@@ -775,6 +775,32 @@ func (hs *HTTPServer) convertModelToDtos(ctx context.Context, ds *datasources.Da
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
//
// 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)
}
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 {
pCtx, err := hs.pluginContextProvider.GetWithDataSource(c.Req.Context(), ds.Type, c.SignedInUser, ds)
if err != nil {

View File

@@ -21,6 +21,7 @@ type corePlugin struct {
backend.StreamHandler
backend.AdmissionHandler
backend.ConversionHandler
backend.SchemaHandler
}
// 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
}
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{},
"conversion": &grpcplugin.ConversionGRPCPlugin{},
"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...)
}
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.AdmissionClient
grpcplugin.ConversionClient
grpcplugin.InformationClient
pluginextensionv2.RendererPlugin
}
@@ -69,6 +70,11 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
return nil, err
}
rawInformation, err := rpcClient.Dispense("information")
if err != nil {
return nil, err
}
c := &ClientV2{}
if rawDiagnostics != nil {
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 err := descriptor.startRendererFn(descriptor.pluginID, c.RendererPlugin, logger); err != nil {
return nil, err
@@ -336,6 +348,36 @@ func (c *ClientV2) ConvertObjects(ctx context.Context, req *backend.ConversionRe
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,
// 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 {

View File

@@ -252,3 +252,11 @@ func (p *grpcPlugin) ConvertObjects(ctx context.Context, request *backend.Conver
}
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.ConversionHandler
backend.StreamHandler
backend.SchemaHandler
}
type Target string

View File

@@ -253,6 +253,36 @@ func (s *Service) ConvertObjects(ctx context.Context, req *backend.ConversionReq
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.
func (s *Service) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
if req == nil {

View File

@@ -76,6 +76,7 @@ var (
_ = backend.StreamHandler(&Plugin{})
_ = backend.AdmissionHandler(&Plugin{})
_ = backend.ConversionHandler(&Plugin{})
_ = backend.SchemaHandler(&Plugin{})
)
type AngularMeta struct {
@@ -413,6 +414,14 @@ func (p *Plugin) ConvertObjects(ctx context.Context, req *backend.ConversionRequ
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) {
cleanPath, err := util.CleanRelativePath(name)
if err != nil {
@@ -470,6 +479,7 @@ type PluginClient interface {
backend.AdmissionHandler
backend.ConversionHandler
backend.StreamHandler
backend.SchemaHandler
}
func (p *Plugin) StaticRoute() *StaticRoute {

View File

@@ -401,6 +401,10 @@ func (m *alwaysErrorFuncMiddleware) ConvertObjects(ctx context.Context, req *bac
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.
func newAlwaysErrorMiddleware(err error) backend.HandlerMiddleware {
return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler {

View File

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