mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 01:52:37 +08:00
Rendering: Store render key in remote cache (#22031)
By storing render key in remote cache it will enable image renderer to use public facing url or load balancer url to render images and thereby remove the requirement of image renderer having to use the url of the originating Grafana instance when running HA setup (multiple Grafana instances). Fixes #17704 Ref grafana/grafana-image-renderer#91
This commit is contained in:

committed by
GitHub

parent
9d7c74ef91
commit
d0a80c59f3
@ -143,7 +143,7 @@ func setupScenarioContext(url string) *scenarioContext {
|
|||||||
Delims: macaron.Delims{Left: "[[", Right: "]]"},
|
Delims: macaron.Delims{Left: "[[", Right: "]]"},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
sc.m.Use(middleware.GetContextHandler(nil, nil))
|
sc.m.Use(middleware.GetContextHandler(nil, nil, nil))
|
||||||
|
|
||||||
return sc
|
return sc
|
||||||
}
|
}
|
||||||
|
@ -316,6 +316,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
|
|||||||
m.Use(middleware.GetContextHandler(
|
m.Use(middleware.GetContextHandler(
|
||||||
hs.AuthTokenService,
|
hs.AuthTokenService,
|
||||||
hs.RemoteCacheService,
|
hs.RemoteCacheService,
|
||||||
|
hs.RenderService,
|
||||||
))
|
))
|
||||||
m.Use(middleware.OrgRedirect())
|
m.Use(middleware.OrgRedirect())
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
@ -39,6 +40,7 @@ var (
|
|||||||
func GetContextHandler(
|
func GetContextHandler(
|
||||||
ats models.UserTokenService,
|
ats models.UserTokenService,
|
||||||
remoteCache *remotecache.RemoteCache,
|
remoteCache *remotecache.RemoteCache,
|
||||||
|
renderService rendering.Service,
|
||||||
) macaron.Handler {
|
) macaron.Handler {
|
||||||
return func(c *macaron.Context) {
|
return func(c *macaron.Context) {
|
||||||
ctx := &models.ReqContext{
|
ctx := &models.ReqContext{
|
||||||
@ -62,7 +64,7 @@ func GetContextHandler(
|
|||||||
// then look for api key in session (special case for render calls via api)
|
// then look for api key in session (special case for render calls via api)
|
||||||
// then test if anonymous access is enabled
|
// then test if anonymous access is enabled
|
||||||
switch {
|
switch {
|
||||||
case initContextWithRenderAuth(ctx):
|
case initContextWithRenderAuth(ctx, renderService):
|
||||||
case initContextWithApiKey(ctx):
|
case initContextWithApiKey(ctx):
|
||||||
case initContextWithBasicAuth(ctx, orgId):
|
case initContextWithBasicAuth(ctx, orgId):
|
||||||
case initContextWithAuthProxy(remoteCache, ctx, orgId):
|
case initContextWithAuthProxy(remoteCache, ctx, orgId):
|
||||||
|
@ -557,7 +557,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) {
|
|||||||
sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
|
sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
|
||||||
sc.remoteCacheService = remotecache.NewFakeStore(t)
|
sc.remoteCacheService = remotecache.NewFakeStore(t)
|
||||||
|
|
||||||
sc.m.Use(GetContextHandler(sc.userAuthTokenService, sc.remoteCacheService))
|
sc.m.Use(GetContextHandler(sc.userAuthTokenService, sc.remoteCacheService, nil))
|
||||||
|
|
||||||
sc.m.Use(OrgRedirect())
|
sc.m.Use(OrgRedirect())
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
|
|||||||
sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
|
sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
|
||||||
sc.remoteCacheService = remotecache.NewFakeStore(t)
|
sc.remoteCacheService = remotecache.NewFakeStore(t)
|
||||||
|
|
||||||
sc.m.Use(GetContextHandler(sc.userAuthTokenService, sc.remoteCacheService))
|
sc.m.Use(GetContextHandler(sc.userAuthTokenService, sc.remoteCacheService, nil))
|
||||||
// mock out gc goroutine
|
// mock out gc goroutine
|
||||||
sc.m.Use(OrgRedirect())
|
sc.m.Use(OrgRedirect())
|
||||||
|
|
||||||
|
@ -1,59 +1,32 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
|
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var renderKeysLock sync.Mutex
|
func initContextWithRenderAuth(ctx *m.ReqContext, renderService rendering.Service) bool {
|
||||||
var renderKeys map[string]*m.SignedInUser = make(map[string]*m.SignedInUser)
|
|
||||||
|
|
||||||
func initContextWithRenderAuth(ctx *m.ReqContext) bool {
|
|
||||||
key := ctx.GetCookie("renderKey")
|
key := ctx.GetCookie("renderKey")
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
renderKeysLock.Lock()
|
renderUser, exists := renderService.GetRenderUser(key)
|
||||||
defer renderKeysLock.Unlock()
|
|
||||||
|
|
||||||
renderUser, exists := renderKeys[key]
|
|
||||||
if !exists {
|
if !exists {
|
||||||
ctx.JsonApiErr(401, "Invalid Render Key", nil)
|
ctx.JsonApiErr(401, "Invalid Render Key", nil)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.IsSignedIn = true
|
ctx.IsSignedIn = true
|
||||||
ctx.SignedInUser = renderUser
|
ctx.SignedInUser = &m.SignedInUser{
|
||||||
|
OrgId: renderUser.OrgID,
|
||||||
|
UserId: renderUser.UserID,
|
||||||
|
OrgRole: m.RoleType(renderUser.OrgRole),
|
||||||
|
}
|
||||||
ctx.IsRenderCall = true
|
ctx.IsRenderCall = true
|
||||||
ctx.LastSeenAt = time.Now()
|
ctx.LastSeenAt = time.Now()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddRenderAuthKey(orgId int64, userId int64, orgRole m.RoleType) (string, error) {
|
|
||||||
renderKeysLock.Lock()
|
|
||||||
defer renderKeysLock.Unlock()
|
|
||||||
|
|
||||||
key, err := util.GetRandomString(32)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
renderKeys[key] = &m.SignedInUser{
|
|
||||||
OrgId: orgId,
|
|
||||||
OrgRole: orgRole,
|
|
||||||
UserId: userId,
|
|
||||||
}
|
|
||||||
|
|
||||||
return key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveRenderAuthKey(key string) {
|
|
||||||
renderKeysLock.Lock()
|
|
||||||
defer renderKeysLock.Unlock()
|
|
||||||
|
|
||||||
delete(renderKeys, key)
|
|
||||||
}
|
|
||||||
|
@ -304,6 +304,10 @@ func (s *testRenderService) RenderErrorImage(err error) (*rendering.RenderResult
|
|||||||
return &rendering.RenderResult{FilePath: "image.png"}, nil
|
return &rendering.RenderResult{FilePath: "image.png"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *testRenderService) GetRenderUser(key string) (*rendering.RenderUser, bool) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
var _ rendering.Service = &testRenderService{}
|
var _ rendering.Service = &testRenderService{}
|
||||||
|
|
||||||
type testImageUploader struct {
|
type testImageUploader struct {
|
||||||
|
@ -26,7 +26,7 @@ var netClient = &http.Client{
|
|||||||
Transport: netTransport,
|
Transport: netTransport,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*RenderResult, error) {
|
func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) {
|
||||||
filePath, err := rs.getFilePathForNewImage()
|
filePath, err := rs.getFilePathForNewImage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -37,11 +37,6 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
renderKey, err := rs.getRenderKey(opts.OrgId, opts.UserId, opts.OrgRole)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
queryParams := rendererUrl.Query()
|
queryParams := rendererUrl.Query()
|
||||||
queryParams.Add("url", rs.getURL(opts.Path))
|
queryParams.Add("url", rs.getURL(opts.Path))
|
||||||
queryParams.Add("renderKey", renderKey)
|
queryParams.Add("renderKey", renderKey)
|
||||||
|
@ -29,9 +29,10 @@ type RenderResult struct {
|
|||||||
FilePath string
|
FilePath string
|
||||||
}
|
}
|
||||||
|
|
||||||
type renderFunc func(ctx context.Context, options Opts) (*RenderResult, error)
|
type renderFunc func(ctx context.Context, renderKey string, options Opts) (*RenderResult, error)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Render(ctx context.Context, opts Opts) (*RenderResult, error)
|
Render(ctx context.Context, opts Opts) (*RenderResult, error)
|
||||||
RenderErrorImage(error error) (*RenderResult, error)
|
RenderErrorImage(error error) (*RenderResult, error)
|
||||||
|
GetRenderUser(key string) (*RenderUser, bool)
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (*RenderResult, error) {
|
func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) {
|
||||||
var executable = "phantomjs"
|
var executable = "phantomjs"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
executable = executable + ".exe"
|
executable = executable + ".exe"
|
||||||
@ -33,12 +32,6 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
renderKey, err := middleware.AddRenderAuthKey(opts.OrgId, opts.UserId, opts.OrgRole)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer middleware.RemoveRenderAuthKey(renderKey)
|
|
||||||
|
|
||||||
phantomDebugArg := "--debug=false"
|
phantomDebugArg := "--debug=false"
|
||||||
if log.GetLogLevelFor("rendering") >= log.LvlDebug {
|
if log.GetLogLevelFor("rendering") >= log.LvlDebug {
|
||||||
phantomDebugArg = "--debug=true"
|
phantomDebugArg = "--debug=true"
|
||||||
|
@ -12,17 +12,12 @@ func (rs *RenderingService) startPlugin(ctx context.Context) error {
|
|||||||
return rs.pluginInfo.Start(ctx)
|
return rs.pluginInfo.Start(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) renderViaPlugin(ctx context.Context, opts Opts) (*RenderResult, error) {
|
func (rs *RenderingService) renderViaPlugin(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) {
|
||||||
pngPath, err := rs.getFilePathForNewImage()
|
pngPath, err := rs.getFilePathForNewImage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
renderKey, err := rs.getRenderKey(opts.OrgId, opts.UserId, opts.OrgRole)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// gives plugin some additional time to timeout and return possible errors.
|
// gives plugin some additional time to timeout and return possible errors.
|
||||||
ctx, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
|
ctx, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -6,9 +6,11 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
@ -17,11 +19,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
remotecache.Register(&RenderUser{})
|
||||||
registry.RegisterService(&RenderingService{})
|
registry.RegisterService(&RenderingService{})
|
||||||
}
|
}
|
||||||
|
|
||||||
var IsPhantomJSEnabled = false
|
var IsPhantomJSEnabled = false
|
||||||
|
|
||||||
|
const renderKeyPrefix = "render-%s"
|
||||||
|
|
||||||
|
type RenderUser struct {
|
||||||
|
OrgID int64
|
||||||
|
UserID int64
|
||||||
|
OrgRole string
|
||||||
|
}
|
||||||
|
|
||||||
type RenderingService struct {
|
type RenderingService struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
pluginInfo *plugins.RendererPlugin
|
pluginInfo *plugins.RendererPlugin
|
||||||
@ -29,7 +40,8 @@ type RenderingService struct {
|
|||||||
domain string
|
domain string
|
||||||
inProgressCount int
|
inProgressCount int
|
||||||
|
|
||||||
Cfg *setting.Cfg `inject:""`
|
Cfg *setting.Cfg `inject:""`
|
||||||
|
RemoteCacheService *remotecache.RemoteCache `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) Init() error {
|
func (rs *RenderingService) Init() error {
|
||||||
@ -103,19 +115,40 @@ func (rs *RenderingService) Render(ctx context.Context, opts Opts) (*RenderResul
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
|
||||||
rs.inProgressCount -= 1
|
|
||||||
}()
|
|
||||||
|
|
||||||
rs.inProgressCount += 1
|
|
||||||
|
|
||||||
if rs.renderAction != nil {
|
if rs.renderAction != nil {
|
||||||
rs.log.Info("Rendering", "path", opts.Path)
|
rs.log.Info("Rendering", "path", opts.Path)
|
||||||
return rs.renderAction(ctx, opts)
|
renderKey, err := rs.generateAndStoreRenderKey(opts.OrgId, opts.UserId, opts.OrgRole)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rs.deleteRenderKey(renderKey)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
rs.inProgressCount--
|
||||||
|
}()
|
||||||
|
|
||||||
|
rs.inProgressCount++
|
||||||
|
return rs.renderAction(ctx, renderKey, opts)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("No renderer found")
|
return nil, fmt.Errorf("No renderer found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rs *RenderingService) GetRenderUser(key string) (*RenderUser, bool) {
|
||||||
|
val, err := rs.RemoteCacheService.Get(fmt.Sprintf(renderKeyPrefix, key))
|
||||||
|
if err != nil {
|
||||||
|
rs.log.Error("Failed to get render key from cache", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val != nil {
|
||||||
|
if user, ok := val.(*RenderUser); ok {
|
||||||
|
return user, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) getFilePathForNewImage() (string, error) {
|
func (rs *RenderingService) getFilePathForNewImage() (string, error) {
|
||||||
rand, err := util.GetRandomString(20)
|
rand, err := util.GetRandomString(20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -152,6 +185,27 @@ func (rs *RenderingService) getURL(path string) string {
|
|||||||
return fmt.Sprintf("%s://%s:%s/%s&render=1", protocol, rs.domain, setting.HttpPort, path)
|
return fmt.Sprintf("%s://%s:%s/%s&render=1", protocol, rs.domain, setting.HttpPort, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) getRenderKey(orgId, userId int64, orgRole models.RoleType) (string, error) {
|
func (rs *RenderingService) generateAndStoreRenderKey(orgId, userId int64, orgRole models.RoleType) (string, error) {
|
||||||
return middleware.AddRenderAuthKey(orgId, userId, orgRole)
|
key, err := util.GetRandomString(32)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rs.RemoteCacheService.Set(fmt.Sprintf(renderKeyPrefix, key), &RenderUser{
|
||||||
|
OrgID: orgId,
|
||||||
|
UserID: userId,
|
||||||
|
OrgRole: string(orgRole),
|
||||||
|
}, 5*time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RenderingService) deleteRenderKey(key string) {
|
||||||
|
err := rs.RemoteCacheService.Delete(fmt.Sprintf(renderKeyPrefix, key))
|
||||||
|
if err != nil {
|
||||||
|
rs.log.Error("Failed to delete render key", "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user