Image Rendering: Remove PhantomJS support (#23460)

Removes all references and usage of PhantomJS #23375.
Remove direct link rendered image e2e smoke test for now.
Docker: Fix installing chrome in ubuntu custom docker image.
Improve handling of image renderer not available/installed #23593.
Add PhantomJS breaking change and upgrading notes.
Use grabpl v0.2.10.

Closes #13802

Co-authored-by: Kyle Brandt <kyle@grafana.com>
Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
This commit is contained in:
Marcus Efraimsson
2020-04-15 22:17:41 +02:00
committed by GitHub
co-authored by Kyle Brandt Arve Knudsen Diana Payton
parent 0a1ab60b8c
commit 6e313e7d37
38 changed files with 107 additions and 478 deletions
+1 -2
View File
@@ -4,7 +4,6 @@ import (
"strconv"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/util"
@@ -212,7 +211,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"licenseUrl": hs.License.LicenseURL(c.SignedInUser),
},
"featureToggles": hs.Cfg.FeatureToggles,
"phantomJSRenderer": rendering.IsPhantomJSEnabled,
"rendererAvailable": hs.RenderService.IsAvailable(),
}
return jsonObj, nil
+1 -1
View File
@@ -42,7 +42,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
appURL := setting.AppUrl
appSubURL := setting.AppSubUrl
// special case when doing localhost call from phantomjs
// special case when doing localhost call from image renderer
if c.IsRenderCall && !hs.Cfg.ServeFromSubPath {
appURL = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort)
appSubURL = ""
+17 -11
View File
@@ -53,19 +53,25 @@ func (n *notificationService) SendIfNeeded(evalCtx *EvalContext) error {
}
if notifierStates.ShouldUploadImage() {
// Create a copy of EvalContext and give it a new, shorter, timeout context to upload the image
uploadEvalCtx := *evalCtx
timeout := setting.AlertingNotificationTimeout / 2
var uploadCtxCancel func()
uploadEvalCtx.Ctx, uploadCtxCancel = context.WithTimeout(evalCtx.Ctx, timeout)
if n.renderService.IsAvailable() {
// Create a copy of EvalContext and give it a new, shorter, timeout context to upload the image
uploadEvalCtx := *evalCtx
timeout := setting.AlertingNotificationTimeout / 2
var uploadCtxCancel func()
uploadEvalCtx.Ctx, uploadCtxCancel = context.WithTimeout(evalCtx.Ctx, timeout)
// Try to upload the image without consuming all the time allocated for EvalContext
if err = n.renderAndUploadImage(&uploadEvalCtx, timeout); err != nil {
n.log.Error("Failed to render and upload alert panel image.", "ruleId", uploadEvalCtx.Rule.ID, "error", err)
// Try to upload the image without consuming all the time allocated for EvalContext
if err = n.renderAndUploadImage(&uploadEvalCtx, timeout); err != nil {
n.log.Error("Failed to render and upload alert panel image.", "ruleId", uploadEvalCtx.Rule.ID, "error", err)
}
uploadCtxCancel()
evalCtx.ImageOnDiskPath = uploadEvalCtx.ImageOnDiskPath
evalCtx.ImagePublicURL = uploadEvalCtx.ImagePublicURL
} else {
n.log.Warn("Could not render image for alert notification, no image renderer found/installed. " +
"For image rendering support please install the grafana-image-renderer plugin. " +
"Read more at https://grafana.com/docs/grafana/latest/administration/image_rendering/")
}
uploadCtxCancel()
evalCtx.ImageOnDiskPath = uploadEvalCtx.ImageOnDiskPath
evalCtx.ImagePublicURL = uploadEvalCtx.ImagePublicURL
}
return n.sendNotifications(evalCtx, notifierStates)
+25
View File
@@ -39,6 +39,16 @@ func TestNotificationService(t *testing.T) {
require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't")
})
notificationServiceScenario(t, "Given alert rule with upload image enabled but no renderer available should not render and upload image, but send notification", evalCtx, true, func(scenarioCtx *scenarioContext) {
scenarioCtx.rendererAvailable = false
err := scenarioCtx.notificationService.SendIfNeeded(evalCtx)
require.NoError(t, err)
require.Equalf(t, 0, scenarioCtx.renderCount, "expected render to not be called, but it was")
require.Equalf(t, 0, scenarioCtx.imageUploadCount, "expected image to not be uploaded, but it was")
require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't")
})
notificationServiceScenario(t, "Given alert rule with upload image disabled should not render and upload image, but send notification", evalCtx, false, func(scenarioCtx *scenarioContext) {
err := scenarioCtx.notificationService.SendIfNeeded(evalCtx)
require.NoError(t, err)
@@ -114,6 +124,7 @@ type scenarioContext struct {
renderCount int
uploadProvider func(ctx context.Context, path string) (string, error)
renderProvider func(ctx context.Context, opts rendering.Opts) (*rendering.RenderResult, error)
rendererAvailable bool
}
type scenarioFunc func(c *scenarioContext)
@@ -197,7 +208,12 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext
return &rendering.RenderResult{FilePath: "image.png"}, nil
}
scenarioCtx.rendererAvailable = true
renderService := &testRenderService{
isAvailableProvider: func() bool {
return scenarioCtx.rendererAvailable
},
renderProvider: func(ctx context.Context, opts rendering.Opts) (*rendering.RenderResult, error) {
if scenarioCtx.renderProvider != nil {
if _, err := scenarioCtx.renderProvider(ctx, opts); err != nil {
@@ -286,10 +302,19 @@ func (n *testNotifier) GetFrequency() time.Duration {
var _ Notifier = &testNotifier{}
type testRenderService struct {
isAvailableProvider func() bool
renderProvider func(ctx context.Context, opts rendering.Opts) (*rendering.RenderResult, error)
renderErrorImageProvider func(error error) (*rendering.RenderResult, error)
}
func (s *testRenderService) IsAvailable() bool {
if s.isAvailableProvider != nil {
return s.isAvailableProvider()
}
return true
}
func (s *testRenderService) Render(ctx context.Context, opts rendering.Opts) (*rendering.RenderResult, error) {
if s.renderProvider != nil {
return s.renderProvider(ctx, opts)
+1
View File
@@ -32,6 +32,7 @@ type RenderResult struct {
type renderFunc func(ctx context.Context, renderKey string, options Opts) (*RenderResult, error)
type Service interface {
IsAvailable() bool
Render(ctx context.Context, opts Opts) (*RenderResult, error)
RenderErrorImage(error error) (*RenderResult, error)
GetRenderUser(key string) (*RenderUser, bool)
-129
View File
@@ -1,129 +0,0 @@
package rendering
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/log"
)
func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) {
var executable = "phantomjs"
if runtime.GOOS == "windows" {
executable = executable + ".exe"
}
url := rs.getURL(opts.Path)
binPath, _ := filepath.Abs(filepath.Join(rs.Cfg.PhantomDir, executable))
if _, err := os.Stat(binPath); os.IsNotExist(err) {
rs.log.Error("executable not found", "executable", binPath)
return nil, ErrPhantomJSNotInstalled
}
scriptPath, _ := filepath.Abs(filepath.Join(rs.Cfg.PhantomDir, "render.js"))
pngPath, err := rs.getFilePathForNewImage()
if err != nil {
return nil, err
}
phantomDebugArg := "--debug=false"
if log.GetLogLevelFor("rendering") >= log.LvlDebug {
phantomDebugArg = "--debug=true"
}
cmdArgs := []string{
"--ignore-ssl-errors=true",
"--web-security=true",
"--local-url-access=false",
phantomDebugArg,
scriptPath,
fmt.Sprintf("url=%v", url),
fmt.Sprintf("width=%v", opts.Width),
fmt.Sprintf("height=%v", opts.Height),
fmt.Sprintf("png=%v", pngPath),
fmt.Sprintf("domain=%v", rs.domain),
fmt.Sprintf("timeout=%v", opts.Timeout.Seconds()),
fmt.Sprintf("renderKey=%v", renderKey),
}
if opts.Encoding != "" {
cmdArgs = append([]string{fmt.Sprintf("--output-encoding=%s", opts.Encoding)}, cmdArgs...)
}
// gives phantomjs some additional time to timeout and return possible errors.
commandCtx, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
defer cancel()
cmd := exec.CommandContext(commandCtx, binPath, cmdArgs...)
cmd.Stderr = cmd.Stdout
timezone := ""
cmd.Env = os.Environ()
if opts.Timezone != "" {
timezone = isoTimeOffsetToPosixTz(opts.Timezone)
cmd.Env = appendEnviron(cmd.Env, "TZ", timezone)
}
// Added to disable usage of newer version of OPENSSL
// that seem to be incompatible with PhantomJS (used in Debian Buster)
if runtime.GOOS == "linux" {
disableNewOpenssl := "/etc/ssl"
cmd.Env = appendEnviron(cmd.Env, "OPENSSL_CONF", disableNewOpenssl)
}
rs.log.Debug("executing Phantomjs", "binPath", binPath, "cmdArgs", cmdArgs, "timezone", timezone)
out, err := cmd.Output()
if out != nil {
rs.log.Debug("Phantomjs output", "out", string(out))
}
if err != nil {
rs.log.Debug("Phantomjs error", "error", err)
}
// check for timeout first
if commandCtx.Err() == context.DeadlineExceeded {
rs.log.Info("Rendering timed out")
return nil, ErrTimeout
}
if err != nil {
rs.log.Error("Phantomjs exited with non zero exit code", "error", err)
return nil, err
}
rs.log.Debug("Image rendered", "path", pngPath)
return &RenderResult{FilePath: pngPath}, nil
}
func isoTimeOffsetToPosixTz(isoOffset string) string {
// invert offset
if strings.HasPrefix(isoOffset, "UTC+") {
return strings.Replace(isoOffset, "UTC+", "UTC-", 1)
}
if strings.HasPrefix(isoOffset, "UTC-") {
return strings.Replace(isoOffset, "UTC-", "UTC+", 1)
}
return isoOffset
}
func appendEnviron(baseEnviron []string, name string, value string) []string {
results := make([]string, 0)
prefix := fmt.Sprintf("%s=", name)
for _, v := range baseEnviron {
if !strings.HasPrefix(v, prefix) {
results = append(results, v)
}
}
return append(results, fmt.Sprintf("%s=%s", name, value))
}
+28 -17
View File
@@ -6,6 +6,7 @@ import (
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/remotecache"
@@ -23,8 +24,6 @@ func init() {
registry.RegisterService(&RenderingService{})
}
var IsPhantomJSEnabled = false
const renderKeyPrefix = "render-%s"
type RenderUser struct {
@@ -76,30 +75,31 @@ func (rs *RenderingService) Run(ctx context.Context) error {
return nil
}
if plugins.Renderer == nil {
rs.log = rs.log.New("renderer", "phantomJS")
rs.log.Info("Backend rendering via phantomJS")
rs.log.Warn("phantomJS is deprecated and will be removed in a future release. " +
"You should consider migrating from phantomJS to grafana-image-renderer plugin. " +
"Read more at https://grafana.com/docs/grafana/latest/administration/image_rendering/")
rs.renderAction = rs.renderViaPhantomJS
IsPhantomJSEnabled = true
if plugins.Renderer != nil {
rs.log = rs.log.New("renderer", "plugin")
rs.pluginInfo = plugins.Renderer
if err := rs.startPlugin(ctx); err != nil {
return err
}
rs.renderAction = rs.renderViaPlugin
<-ctx.Done()
return nil
}
rs.log = rs.log.New("renderer", "plugin")
rs.pluginInfo = plugins.Renderer
rs.log.Debug("No image renderer found/installed. " +
"For image rendering support please install the grafana-image-renderer plugin. " +
"Read more at https://grafana.com/docs/grafana/latest/administration/image_rendering/")
if err := rs.startPlugin(ctx); err != nil {
return err
}
rs.renderAction = rs.renderViaPlugin
<-ctx.Done()
return nil
}
func (rs *RenderingService) IsAvailable() bool {
return rs.renderAction != nil
}
func (rs *RenderingService) RenderErrorImage(err error) (*RenderResult, error) {
imgUrl := "public/img/rendering_error.png"
@@ -209,3 +209,14 @@ func (rs *RenderingService) deleteRenderKey(key string) {
rs.log.Error("Failed to delete render key", "error", err)
}
}
func isoTimeOffsetToPosixTz(isoOffset string) string {
// invert offset
if strings.HasPrefix(isoOffset, "UTC+") {
return strings.Replace(isoOffset, "UTC+", "UTC-", 1)
}
if strings.HasPrefix(isoOffset, "UTC-") {
return strings.Replace(isoOffset, "UTC-", "UTC+", 1)
}
return isoOffset
}
-2
View File
@@ -239,7 +239,6 @@ type Cfg struct {
// Rendering
ImagesDir string
PhantomDir string
RendererUrl string
RendererCallbackUrl string
RendererLimit int
@@ -940,7 +939,6 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
}
}
cfg.ImagesDir = filepath.Join(cfg.DataPath, "png")
cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true)
cfg.MetricsEndpointBasicAuthUsername, err = valueAsString(iniFile.Section("metrics"), "basic_auth_username", "")