grafana-image-renderer: 4.0.14 -> 5.0.10 (#461983)

This commit is contained in:
Maximilian Bosch
2025-11-21 15:40:02 +00:00
committed by GitHub
3 changed files with 194 additions and 257 deletions
@@ -2,97 +2,178 @@
lib,
pkgs,
config,
utils,
...
}:
let
cfg = config.services.grafana-image-renderer;
format = pkgs.formats.json { };
format = {
type =
with lib.types;
attrsOf (
attrsOf (oneOf [
str
int
bool
(listOf (oneOf [
str
int
]))
])
);
configFile = format.generate "grafana-image-renderer-config.json" cfg.settings;
generate = lib.flip lib.pipe [
# Remove legacy option prefixes that only exist for backwards-compat
(lib.flip builtins.removeAttrs [
"service"
"rendering"
"assertions"
"warnings"
])
# Normalize CLI args from a nested attr-set to `section.option = value`
(lib.concatMapAttrs (block: lib.mapAttrs' (option: lib.nameValuePair "${block}.${option}")))
# Turn attr-set into a list of arguments, denormalize list options.
(lib.mapAttrsToList (
option: value:
if lib.isBool value then
lib.optional value "--${option}"
else if lib.isList value then
map (v: [
"--${option}"
v
]) value
else
[
"--${option}"
(toString value)
]
))
lib.flatten
# Turn into a string
utils.escapeSystemdExecArgs
];
};
in
{
imports = [
(lib.mkChangedOptionModule
[
"services"
"grafana-image-renderer"
"chromium"
]
[
"services"
"grafana-image-renderer"
"settings"
"browser"
"path"
]
(config: lib.getExe config.services.grafana-image-renderer.chromium)
)
(lib.mkRemovedOptionModule
[
"services"
"grafana-image-renderer"
"verbose"
]
''
Use `services.grafana-image-renderer.settings.log.level = "debug"` instead.
''
)
];
options.services.grafana-image-renderer = {
enable = lib.mkEnableOption "grafana-image-renderer";
chromium = lib.mkOption {
type = lib.types.package;
description = ''
The chromium to use for image rendering.
'';
};
verbose = lib.mkEnableOption "verbosity for the service";
provisionGrafana = lib.mkEnableOption "Grafana configuration for grafana-image-renderer";
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = format.type;
imports = [
../../misc/assertions.nix
(lib.mkRenamedOptionModule
[
"rendering"
"width"
]
[
"browser"
"min-width"
]
)
(lib.mkRenamedOptionModule
[
"rendering"
"height"
]
[
"browser"
"min-height"
]
)
(lib.mkRenamedOptionModule
[
"rendering"
"args"
]
[
"browser"
"flag"
]
)
(lib.mkChangedOptionModule
[
"service"
"port"
]
[
"server"
"addr"
]
(config: "0.0.0.0:${toString config.service.port}")
)
(lib.mkRemovedOptionModule
[
"rendering"
"mode"
]
''
This option is obsolete.
''
)
(lib.mkRenamedOptionModule
[
"service"
"logging"
]
[
"log"
]
)
];
options = {
service = {
port = lib.mkOption {
type = lib.types.port;
default = 8081;
description = ''
The TCP port to use for the rendering server.
'';
};
logging.level = lib.mkOption {
type = lib.types.enum [
"error"
"warning"
"info"
"debug"
];
default = "info";
description = ''
The log-level of the {file}`grafana-image-renderer.service`-unit.
'';
};
server.addr = lib.mkOption {
type = lib.types.str;
default = "localhost:8081";
description = ''
Listen address of the service.
'';
};
rendering = {
width = lib.mkOption {
default = 1000;
type = lib.types.ints.positive;
description = ''
Width of the PNG used to display the alerting graph.
'';
};
height = lib.mkOption {
default = 500;
type = lib.types.ints.positive;
description = ''
Height of the PNG used to display the alerting graph.
'';
};
mode = lib.mkOption {
default = "default";
type = lib.types.enum [
"default"
"reusable"
"clustered"
];
description = ''
Rendering mode of `grafana-image-renderer`:
- `default:` Creates on browser-instance
per rendering request.
- `reusable:` One browser instance
will be started and reused for each rendering request.
- `clustered:` allows to precisely
configure how many browser-instances are supposed to be used. The values
for that mode can be declared in `rendering.clustering`.
'';
};
args = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "--no-sandbox" ];
description = ''
List of CLI flags passed to `chromium`.
'';
};
browser.path = lib.mkOption {
type = lib.types.path;
default = lib.getExe pkgs.chromium;
defaultText = lib.literalExpression "lib.getExe pkgs.chromium";
description = ''
Path to the executable of the chromium to use.
'';
};
};
};
@@ -101,9 +182,6 @@ in
description = ''
Configuration attributes for `grafana-image-renderer`.
See <https://github.com/grafana/grafana-image-renderer/blob/ce1f81438e5f69c7fd7c73ce08bab624c4c92e25/default.json>
for supported values.
'';
};
};
@@ -120,39 +198,53 @@ in
];
services.grafana.settings.rendering = lib.mkIf cfg.provisionGrafana {
server_url = "http://localhost:${toString cfg.settings.service.port}/render";
server_url = "http://${toString cfg.settings.server.addr}/render";
callback_url = "http://${config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}";
};
services.grafana-image-renderer.chromium = lib.mkDefault pkgs.chromium;
services.grafana-image-renderer.settings = {
rendering = lib.mapAttrs (lib.const lib.mkDefault) {
chromeBin = "${cfg.chromium}/bin/chromium";
verboseLogging = cfg.verbose;
timezone = config.time.timeZone;
};
service = {
logging.level = lib.mkIf cfg.verbose (lib.mkDefault "debug");
metrics.enabled = lib.mkDefault false;
};
browser.timezone = lib.mkIf (config.time.timeZone != null) (lib.mkDefault config.time.timeZone);
};
systemd.services.grafana-image-renderer = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
description = "Grafana backend plugin that handles rendering of panels & dashboards to PNGs using headless browser (Chromium/Chrome)";
environment = {
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = "true";
};
serviceConfig = {
DynamicUser = true;
PrivateTmp = true;
ExecStart = "${pkgs.grafana-image-renderer}/bin/grafana-image-renderer server --config=${configFile}";
ExecStart = "${lib.getExe pkgs.grafana-image-renderer} server ${format.generate cfg.settings}";
Restart = "always";
AmbientCapabilities = "";
CapabilityBoundingSet = "";
LockPersonality = true;
MountAPIVFS = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = "strict";
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "full";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
UMask = 27;
};
};
};
@@ -1,109 +0,0 @@
{
"name": "renderer",
"version": "1.0.0",
"author": "Grafana Labs",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/grafana/grafana-image-renderer.git"
},
"scripts": {
"eslint": "eslint .",
"typecheck": "tsc --noEmit",
"prettier:check": "prettier --list-different \"**/*.ts\"",
"prettier:write": "prettier --list-different \"**/*.ts\" --write",
"precommit": "npm run eslint & npm run typecheck",
"watch": "tsc-watch --onSuccess \"node build/app.js server --config=dev.json\"",
"watch:debug": "tsc-watch --onSuccess \"cross-env DEBUG=puppeteer-cluster:* node build/app.js server --config=dev.json\"",
"build": "tsc",
"start": "node build/app.js server --config=dev.json",
"create-gcom-plugin-json": "ts-node scripts/createGcomPluginJson.ts ./scripts/tmp",
"push-to-gcom": "sh ./scripts/push-to-gcom.sh",
"test-update": "cross-env UPDATE_GOLDEN=true jest",
"test": "sh ./scripts/run_tests.sh",
"test-ci": "jest",
"test-diff": "cross-env SAVE_DIFF=true jest"
},
"dependencies": {
"@grpc/grpc-js": "^1.8.22",
"@grpc/proto-loader": "^0.7.2",
"@hapi/boom": "^10.0.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.49.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
"@opentelemetry/resources": "^1.25.1",
"@opentelemetry/sdk-node": "^0.52.1",
"@opentelemetry/semantic-conventions": "^1.25.1",
"@puppeteer/browsers": "^2.3.1",
"chokidar": "^3.5.2",
"dompurify": "^3.2.4",
"express": "^4.21.1",
"express-prom-bundle": "^6.5.0",
"ioredis": "^5.6.1",
"jimp": "^0.22.12",
"jsdom": "20.0.0",
"lodash": "^4.17.21",
"minimist": "^1.2.6",
"morgan": "^1.9.0",
"multer": "^2.0.2",
"on-finished": "^2.3.0",
"poolpeteer": "^0.24.0",
"prom-client": "^14.1.0",
"puppeteer": "^22.8.2",
"puppeteer-cluster": "^0.24.0",
"rate-limiter-flexible": "^7.0.0",
"tar-fs": "^3.0.9",
"unique-filename": "^2.0.1",
"winston": "^3.8.2"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@grafana/eslint-config": "^8.1.0",
"@grafana/sign-plugin": "^3.1.3",
"@stylistic/eslint-plugin-ts": "^4.4.1",
"@types/content-disposition": "^0.5.9",
"@types/express": "^4.17.14",
"@types/jest": "^29.5.12",
"@types/jsdom": "20.0.0",
"@types/lodash": "^4.17.20",
"@types/minimist": "^1.2.5",
"@types/morgan": "^1.9.10",
"@types/multer": "^1.4.7",
"@types/node": "^20.17.27",
"@types/pixelmatch": "^5.2.6",
"@types/supertest": "^2.0.15",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.37.0",
"@yao-pkg/pkg": "^6.3.0",
"axios": "1.8.2",
"cross-env": "7.0.3",
"eslint": "^9.31.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-jsdoc": "^51.4.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"fast-png": "^6.2.0",
"jest": "^29.7.0",
"jsonwebtoken": "^9.0.2",
"lint-staged": "13.0.3",
"prettier": "2.7.1",
"supertest": "^7.0.0",
"ts-jest": "^29.1.1",
"ts-node": "10.9.1",
"tsc-watch": "5.0.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.37.0"
},
"lint-staged": {
"*.ts": [
"prettier --write"
]
},
"pkg": {
"assets": "proto/*"
},
"bin": "build/app.js",
"engines": {
"node": ">= 22"
}
}
@@ -1,68 +1,23 @@
{
lib,
mkYarnPackage,
buildGoModule,
fetchFromGitHub,
fetchYarnDeps,
nodejs,
runtimeShell,
}:
# Notes for the upgrade:
# * Download the tarball of the new version to use.
# * Replace new `package.json` here.
# * Update `version`+`hash` and rebuild.
mkYarnPackage rec {
buildGoModule (finalAttrs: {
pname = "grafana-image-renderer";
version = "4.0.14";
version = "5.0.10";
src = fetchFromGitHub {
owner = "grafana";
repo = "grafana-image-renderer";
rev = "v${version}";
hash = "sha256-CoQTOzQ7h31B3U0yvJYsgC3uaSyjNNLpD+8uMN+naiQ=";
tag = "v${finalAttrs.version}";
hash = "sha256-oWJlb1mV1sNgN7EQ8L4msfnKps5oV60JgwZYpAJQaq4=";
};
offlineCache = fetchYarnDeps {
yarnLock = src + "/yarn.lock";
hash = "sha256-xZrIoQlPeyGTbFRUQ0M8Tc6YpzsC5IACW0bbZ+HnsOQ=";
};
vendorHash = "sha256-wA1XeLO2bYwq7HZOQ5UNcdqqJdEWRUxFoAQucXAj48k=";
packageJSON = ./package.json;
buildPhase = ''
runHook preBuild
pushd deps/renderer
yarn run build
popd
runHook postBuild
'';
dontInstall = true;
distPhase = ''
runHook preDist
shopt -s extglob
pushd deps/renderer
install_path="$out/libexec/grafana-image-renderer"
mkdir -p $install_path
cp -R ../../node_modules $install_path
cp -R ./!(node_modules) $install_path
popd
mkdir -p $out/bin
cat >$out/bin/grafana-image-renderer <<EOF
#! ${runtimeShell}
${nodejs}/bin/node $install_path/build/app.js \$@
EOF
chmod +x $out/bin/grafana-image-renderer
runHook postDist
'';
subPackages = [ "." ];
meta = with lib; {
homepage = "https://github.com/grafana/grafana-image-renderer";
@@ -70,6 +25,5 @@ mkYarnPackage rec {
mainProgram = "grafana-image-renderer";
license = licenses.asl20;
maintainers = with maintainers; [ ma27 ];
platforms = platforms.all;
};
}
})