Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handler middlewares #1019

Merged
merged 6 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
)

const (
// EndpointConvertObject friendly name for the convert object endpoint/handler.
EndpointConvertObject Endpoint = "convertObject"
// EndpointConvertObjects friendly name for the convert objects endpoint/handler.
EndpointConvertObjects Endpoint = "convertObjects"
)

// ConversionHandler is an EXPERIMENTAL service that allows converting objects between versions
Expand Down
2 changes: 1 addition & 1 deletion backend/conversion_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (a *conversionSDKAdapter) convertQueryDataRequest(ctx context.Context, requ
}

func (a *conversionSDKAdapter) ConvertObjects(ctx context.Context, req *pluginv2.ConversionRequest) (*pluginv2.ConversionResponse, error) {
ctx = setupContext(ctx, EndpointConvertObject)
ctx = setupContext(ctx, EndpointConvertObjects)
parsedReq := FromProto().ConversionRequest(req)

resp := &ConversionResponse{}
Expand Down
70 changes: 70 additions & 0 deletions backend/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package backend

import "context"

// Handler interface for all handlers.
type Handler interface {
QueryDataHandler
CheckHealthHandler
CallResourceHandler
CollectMetricsHandler
StreamHandler
AdmissionHandler
ConversionHandler
}

var _ = Handler(&BaseHandler{})

// BaseHandler base handler provides a base implementation of Handler interface
// passing the request down the chain to next Handler.
// This allows handlers to avoid implementing the full Handler interface.
type BaseHandler struct {
next Handler
}

// NewBaseHandler creates a new BaseHandler.
func NewBaseHandler(next Handler) BaseHandler {
return BaseHandler{
next: next,
}
}

func (m BaseHandler) QueryData(ctx context.Context, req *QueryDataRequest) (*QueryDataResponse, error) {
return m.next.QueryData(ctx, req)
}

func (m BaseHandler) CallResource(ctx context.Context, req *CallResourceRequest, sender CallResourceResponseSender) error {
return m.next.CallResource(ctx, req, sender)
}

func (m BaseHandler) CheckHealth(ctx context.Context, req *CheckHealthRequest) (*CheckHealthResult, error) {
return m.next.CheckHealth(ctx, req)
}

func (m BaseHandler) CollectMetrics(ctx context.Context, req *CollectMetricsRequest) (*CollectMetricsResult, error) {
return m.next.CollectMetrics(ctx, req)
}

func (m BaseHandler) SubscribeStream(ctx context.Context, req *SubscribeStreamRequest) (*SubscribeStreamResponse, error) {
return m.next.SubscribeStream(ctx, req)
}

func (m BaseHandler) PublishStream(ctx context.Context, req *PublishStreamRequest) (*PublishStreamResponse, error) {
return m.next.PublishStream(ctx, req)
}

func (m BaseHandler) RunStream(ctx context.Context, req *RunStreamRequest, sender *StreamSender) error {
return m.next.RunStream(ctx, req, sender)
}

func (m BaseHandler) ValidateAdmission(ctx context.Context, req *AdmissionRequest) (*ValidationResponse, error) {
return m.next.ValidateAdmission(ctx, req)
}

func (m *BaseHandler) MutateAdmission(ctx context.Context, req *AdmissionRequest) (*MutationResponse, error) {
return m.next.MutateAdmission(ctx, req)
}

func (m *BaseHandler) ConvertObjects(ctx context.Context, req *ConversionRequest) (*ConversionResponse, error) {
return m.next.ConvertObjects(ctx, req)
}
181 changes: 181 additions & 0 deletions backend/handler_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package backend

import (
"context"
"errors"
"slices"
)

var (
errNilRequest = errors.New("req cannot be nil")
errNilSender = errors.New("sender cannot be nil")
)

// HandlerMiddleware is an interface representing the ability to create a middleware
// that implements the Handler interface.
type HandlerMiddleware interface {
// CreateHandlerMiddleware creates a new Handler by decorating next Handler.
CreateHandlerMiddleware(next Handler) Handler
}

// The HandlerMiddlewareFunc type is an adapter to allow the use of ordinary
// functions as HandlerMiddleware's. If f is a function with the appropriate
// signature, HandlerMiddlewareFunc(f) is a HandlerMiddleware that calls f.
type HandlerMiddlewareFunc func(next Handler) Handler

// CreateHandlerMiddleware implements the HandlerMiddleware interface.
func (fn HandlerMiddlewareFunc) CreateHandlerMiddleware(next Handler) Handler {
return fn(next)
}

// MiddlewareHandler decorates a Handler with HandlerMiddleware's.
type MiddlewareHandler struct {
middlewares []HandlerMiddleware
finalHandler Handler
}

// HandlerFromMiddlewares creates a new MiddlewareHandler implementing Handler that decorates finalHandler with middlewares.
func HandlerFromMiddlewares(finalHandler Handler, middlewares ...HandlerMiddleware) (*MiddlewareHandler, error) {
if finalHandler == nil {
return nil, errors.New("finalHandler cannot be nil")
}

return &MiddlewareHandler{
middlewares: middlewares,
finalHandler: finalHandler,
}, nil
}

func (h *MiddlewareHandler) setupContext(ctx context.Context, pluginCtx PluginContext, endpoint Endpoint) context.Context {
ctx = initErrorSource(ctx)
ctx = WithEndpoint(ctx, endpoint)
ctx = WithPluginContext(ctx, pluginCtx)
ctx = WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only now when looking at all this when I realize all we really need to pass through for a lot of these is pluginCtx since these are all exported fields that could be read as needed 🤦

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah 😄 What do you suggest, remove WithGrafanaConfig and use PluginConfigFromContext in GrafanaConfigFromContext? Or remove GrafanaConfigFromContext altogether?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we remove WithGrafanaConfig it'll break Grafana core - and all the core plugins would need to be updated.

Removing GrafanaConfigFromContext will smash a bunch of plugins in the wild.

So I guess for now we do nothing, except cry 😢

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah 😄 On the positive side, the current way allow tests to use backend. WithGrafanaConfig rather than creating a full context.

ctx = WithUser(ctx, pluginCtx.User)
ctx = WithUserAgent(ctx, pluginCtx.UserAgent)
marefr marked this conversation as resolved.
Show resolved Hide resolved
return ctx
}

func (h *MiddlewareHandler) QueryData(ctx context.Context, req *QueryDataRequest) (*QueryDataResponse, error) {
if req == nil {
return nil, errNilRequest
}

ctx = h.setupContext(ctx, req.PluginContext, EndpointQueryData)
handler := handlerFromMiddlewares(h.middlewares, h.finalHandler)
return handler.QueryData(ctx, req)
}

func (h MiddlewareHandler) CallResource(ctx context.Context, req *CallResourceRequest, sender CallResourceResponseSender) error {
if req == nil {
return errNilRequest
}

if sender == nil {
return errNilSender
}

ctx = h.setupContext(ctx, req.PluginContext, EndpointCallResource)
handler := handlerFromMiddlewares(h.middlewares, h.finalHandler)
return handler.CallResource(ctx, req, sender)
}

func (h MiddlewareHandler) CollectMetrics(ctx context.Context, req *CollectMetricsRequest) (*CollectMetricsResult, error) {
if req == nil {
return nil, errNilRequest
}

ctx = h.setupContext(ctx, req.PluginContext, EndpointCollectMetrics)
handler := handlerFromMiddlewares(h.middlewares, h.finalHandler)
return handler.CollectMetrics(ctx, req)
}

func (h MiddlewareHandler) CheckHealth(ctx context.Context, req *CheckHealthRequest) (*CheckHealthResult, error) {
if req == nil {
return nil, errNilRequest
}

ctx = h.setupContext(ctx, req.PluginContext, EndpointCheckHealth)
handler := handlerFromMiddlewares(h.middlewares, h.finalHandler)
return handler.CheckHealth(ctx, req)
}

func (h MiddlewareHandler) SubscribeStream(ctx context.Context, req *SubscribeStreamRequest) (*SubscribeStreamResponse, error) {
if req == nil {
return nil, errNilRequest
}

ctx = h.setupContext(ctx, req.PluginContext, EndpointSubscribeStream)
handler := handlerFromMiddlewares(h.middlewares, h.finalHandler)
return handler.SubscribeStream(ctx, req)
}

func (h MiddlewareHandler) PublishStream(ctx context.Context, req *PublishStreamRequest) (*PublishStreamResponse, error) {
if req == nil {
return nil, errNilRequest
}

ctx = h.setupContext(ctx, req.PluginContext, EndpointPublishStream)
handler := handlerFromMiddlewares(h.middlewares, h.finalHandler)
return handler.PublishStream(ctx, req)
}

func (h MiddlewareHandler) RunStream(ctx context.Context, req *RunStreamRequest, sender *StreamSender) error {
if req == nil {
return errNilRequest
}

if sender == nil {
return errors.New("sender cannot be nil")
}

ctx = h.setupContext(ctx, req.PluginContext, EndpointRunStream)
handler := handlerFromMiddlewares(h.middlewares, h.finalHandler)
return handler.RunStream(ctx, req, sender)
}

func (h MiddlewareHandler) ValidateAdmission(ctx context.Context, req *AdmissionRequest) (*ValidationResponse, error) {
if req == nil {
return nil, errNilRequest
}

ctx = h.setupContext(ctx, req.PluginContext, EndpointValidateAdmission)
handler := handlerFromMiddlewares(h.middlewares, h.finalHandler)
return handler.ValidateAdmission(ctx, req)
}

func (h MiddlewareHandler) MutateAdmission(ctx context.Context, req *AdmissionRequest) (*MutationResponse, error) {
if req == nil {
return nil, errNilRequest
}

ctx = h.setupContext(ctx, req.PluginContext, EndpointMutateAdmission)
handler := handlerFromMiddlewares(h.middlewares, h.finalHandler)
return handler.MutateAdmission(ctx, req)
}

func (h MiddlewareHandler) ConvertObjects(ctx context.Context, req *ConversionRequest) (*ConversionResponse, error) {
if req == nil {
return nil, errNilRequest
}

ctx = h.setupContext(ctx, req.PluginContext, EndpointConvertObjects)
handler := handlerFromMiddlewares(h.middlewares, h.finalHandler)
return handler.ConvertObjects(ctx, req)
}

func handlerFromMiddlewares(middlewares []HandlerMiddleware, finalHandler Handler) Handler {
if len(middlewares) == 0 {
return finalHandler
}

clonedMws := slices.Clone(middlewares)
slices.Reverse(clonedMws)
next := finalHandler

for _, m := range clonedMws {
next = m.CreateHandlerMiddleware(next)
}

return next
}
Loading
Loading