Skip to content

Commit

Permalink
feat: Configure grafana data path from provisioning (#18)
Browse files Browse the repository at this point in the history
* Avoid hard coded paths so that plugin runs on windows as well

* Discover data path based on path of executable if data path is not configured

* Update example provisioned file with more comments

* Update README

Signed-off-by: Mahendra Paipuri <[email protected]>
  • Loading branch information
mahendrapaipuri authored Apr 4, 2024
1 parent 4e12ac6 commit c4ff5f8
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 32 deletions.
46 changes: 32 additions & 14 deletions pkg/plugin/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"github.com/spf13/afero"
)

const GF_PATHS_DATA = "/var/lib/grafana"
const PLUGIN_NAME = "mahendrapaipuri-dashboardreporter-app"

// Make sure App implements required interfaces. This is important to do
Expand Down Expand Up @@ -72,6 +71,7 @@ func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (instance
var data map[string]interface{}
var grafanaAppUrl string
var skipTLSCheck bool = false
var grafanaDataPath string
var orientation string
var layout string
var dashboardMode string
Expand All @@ -85,6 +85,9 @@ func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (instance
if v, exists := data["skipTlsCheck"]; exists {
skipTLSCheck = v.(bool)
}
if v, exists := data["dataPath"]; exists {
grafanaDataPath = v.(string)
}
if v, exists := data["orientation"]; exists {
orientation = v.(string)
}
Expand All @@ -103,6 +106,7 @@ func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (instance
}
ctxLogger.Info(
"provisioned config", "appUrl", grafanaAppUrl, "skipTlsCheck", skipTLSCheck,
"dataPath", grafanaDataPath,
"orientation", orientation, "layout", layout, "dashboardMode", dashboardMode,
"maxRenderWorkers", maxRenderWorkers, "persistData", persistData,
)
Expand All @@ -128,15 +132,22 @@ func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (instance
// Seems like accessing env vars is not encouraged
// Ref: https://github.com/grafana/plugin-validator/blob/eb71abbbead549fd7697371b25c226faba19b252/pkg/analysis/passes/coderules/semgrep-rules.yaml#L13-L28
//
// If appURL is not found in plugin settings attempt to get it from env var
if grafanaAppUrl == "" && os.Getenv("GF_APP_URL") != "" {
// appURL set from the env var will always take the highest precedence
if os.Getenv("GF_APP_URL") != "" {
grafanaAppUrl = strings.TrimRight(os.Getenv("GF_APP_URL"), "/")
ctxLogger.Debug("Using Grafana app URL from environment variable", "GF_APP_URL", grafanaAppUrl)
}

if grafanaAppUrl == "" {
return nil, fmt.Errorf("grafana app URL not configured in JSONData")
}

// Similarly GF_PATHS_DATA set from the env var will always have the highest precedence
if os.Getenv("GF_PATHS_DATA") != "" {
grafanaDataPath = os.Getenv("GF_PATHS_DATA")
ctxLogger.Debug("Using Grafana data path from environment variable", "GF_PATHS_DATA", grafanaDataPath)
}

/*
Create a virtual FS with /var/lib/grafana as base path. In cloud context,
probably this is the only directory with write permissions. We cannot rely
Expand All @@ -147,17 +158,24 @@ func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (instance
into PDF. We will clean them up after each request and so we will use this
reports directory to store these files.
*/
var pluginDir string
if os.Getenv("GF_PATHS_DATA") != "" {
pluginDir = os.Getenv("GF_PATHS_DATA")
} else {
pluginDir = GF_PATHS_DATA
if grafanaDataPath == "" {
// If grafanaDataPath is still not set, attempt to get it from current executable path
// Get path of current executable
pluginExe, err := os.Executable()
if err != nil {
panic(err)
}

// Generally this pluginExe should be at install_dir/plugins/mahendrapaipuri-dashboardreporter-app/exe
// Now we attempt to get install_dir directory which is Grafana data path
grafanaDataPath = filepath.Dir(filepath.Dir(filepath.Dir(pluginExe)))
ctxLogger.Info("Grafana data path found", "GF_PATHS_DATA", grafanaDataPath)
}
vfs := afero.NewBasePathFs(afero.NewOsFs(), pluginDir).(*afero.BasePathFs)
vfs := afero.NewBasePathFs(afero.NewOsFs(), grafanaDataPath).(*afero.BasePathFs)

// Create a reports dir inside this GF_PATHS_DATA folder
if err = vfs.MkdirAll("reports", 0750); err != nil {
return nil, fmt.Errorf("failed to create a reports directory in %s: %w", pluginDir, err)
return nil, fmt.Errorf("failed to create a reports directory in %s: %w", grafanaDataPath, err)
}

// Set chrome options
Expand All @@ -170,8 +188,8 @@ func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (instance
Attempt to use chrome shipped from grafana-image-renderer. If not found,
use the chromium browser installed on the host.
We check for the GF_DATA_PATH env variable and if not found we use default
/var/lib/grafana. We do a walk dir in $GR_DATA_PATH/plugins/grafana-image-render
We check for the GF_PATHS_DATA env variable and if not found we use default
/var/lib/grafana. We do a walk dir in $GF_PATHS_DATA/plugins/grafana-image-render
and try to find `chrome` executable. If we find it, we use it as chrome
executable for rendering the PDF report.
*/
Expand All @@ -180,7 +198,7 @@ func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (instance
var chromeExec string

// Walk through grafana-image-renderer plugin dir to find chrome executable
err = filepath.Walk(filepath.Join(pluginDir, "plugins", "grafana-image-renderer"),
err = filepath.Walk(filepath.Join(grafanaDataPath, "plugins", "grafana-image-renderer"),
func(path string, info fs.FileInfo, err error) error {
// prevent panic by handling failure accessing a path
if err != nil {
Expand All @@ -193,7 +211,7 @@ func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (instance
return nil
})
if err != nil {
ctxLogger.Warn("failed to walk through grafana-image-renderer plugin dir", "err", err)
ctxLogger.Warn("failed to walk through grafana-image-renderer data dir", "err", err)
}

// If chrome is found in grafana-image-renderer plugin dir, use it
Expand Down
74 changes: 74 additions & 0 deletions provisioning/plugins/app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,84 @@ apps:
org_name: Main Org.
disabled: false
jsonData:
# appUrl is at which Grafana can be accessible. The plugin will make API requests
# to Grafana to get individual panel in each dashboard to generate reports. These API
# requests will be made to this URL. For most of the deployments the default value
# will work.
#
# If the Grafana is configured to use cookie_samesite "strict", the default localhost:3000
# will not work as the plugin will forward cookie in the API requests and "strict" policy
# will not make cookie set on URL Grafana is exposed (eg mygrafana.example.com) on
# localhost:3000. In this case, please use appUrl as `mygrafana.example.com`
#
# The app URL set in GF_APP_URL env variable will always have the highest precedence
# and will override the value set here
#
appUrl: http://localhost:3000

# If Grafana is using HTTPS with self signed certificates, set this parameter to true
# to skip TLS certificate verification
#
skipTlsCheck: false

# The plugin uses Grafana data path to discover grafana-image-renderer plugin and also
# to stage the generated PDF files before serving them. In a classic deployment,
# default /var/lib/grafana should work. If the `paths.data` in `grafana.ini` is set
# to custom path, use the same path here
#
# In the windows machines, please set it to the path corresponding to `install_dir/data`
# eg C:\grafana-10.1.0\data
#
# The data path set in GF_PATHS_DATA env variable will always have the highest precedence
# and will override the value set here
#
dataPath: /var/lib/grafana

# Orientation of the report. Possible values are portrait and landscape
#
# This can be changed from Grafana UI as well and this configuration parameter
# applies globally to all generated reports
#
# This setting can be overridden for a particular dashboard by using query parameter
# ?orientation=portrait or ?orientation=landscape during report generation process
#
orientation: portrait

# Layout of the report. Possible values are simple and grid
#
# This can be changed from Grafana UI as well and this configuration parameter
# applies globally to all generated reports
#
# This setting can be overridden for a particular dashboard by using query parameter
# ?layout=simple or ?layout=grid during report generation process
#
layout: simple

# Dashboard mode in the report. Possible values are default and full. In default
# mode collapsed rows will be ignored in the report and only Panels visible in
# dashboard by default will be rendered in the report. In the full mode, all the
# rows will be expanded and all the panels in the dashboard will be included in
# the report.
#
# This can be changed from Grafana UI as well and this configuration parameter
# applies globally to all generated reports
#
# This setting can be overridden for a particular dashboard by using query parameter
# ?dashboardMode=default or ?dashboardMode=full during report generation process
#
dashboardMode: default

# Maximum number of workers for generating panel PNGs.
#
# This can be changed from Grafana UI as well and this configuration parameter
# applies globally to all generated reports
#
maxRenderWorkers: 2

# Persist PNG files, generated HTML files and PDF for debugging. These files can
# be found at $GF_PATHS_DATA/reports/debug folder
#
# This can be changed from Grafana UI as well and this configuration parameter
# applies globally to all generated reports
#
persistData: false
25 changes: 7 additions & 18 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,23 +74,6 @@ cd /var/lib/grafana/plugins
curl https://raw.githubusercontent.com/mahendrapaipuri/grafana-dashboard-reporter-app/main/scripts/bootstrap-dashboard-reporter-app.sh | NIGHTLY=1 bash
```

The plugin uses Grafana data path as a ephemeral directory to generate reports. If
default value for `paths.data` in `grafana.ini`, there is nothing to do, the plugin
should work out-of-the-box. However, if a custom directory is used for `paths.data`,
it is **compulsory** to set an environment variable `GF_PATHS_DATA` that points to the
same directory as `paths.data` in `grafana.ini`. For example, if your `grafana.ini`
looks like below

```
[paths]
data = /path/to/grafana/data
```

the following environment variable must be set when Grafana is started:

```
GF_PATHS_DATA=/path/to/grafana/data
```
> [!IMPORTANT]
> The final step is to _whitelist_ the plugin as it is an unsigned plugin and Grafana,
by default, does not load any unsigned plugins even if they are installed. In order to
Expand Down Expand Up @@ -152,7 +135,13 @@ Different configuration parameters are explained below:
The following configuration parameters are directly tied to Grafana instance

- `appUrl`: The URL at which Grafana is running. By default, `http://localhost:3000` is
used which should work for most of the deployments.
used which should work for most of the deployments. If environment variable `GF_APP_URL`
is set, that will take the precedence over the value configured in the provisioning file.

- `dataPath`: Grafana data path. By default on Linux deployment, it is `/var/lib/grafana`.
If a custom directory is used for `paths.data` in `grafana.ini`, the same path should be
used for the `dataPath`. If environment variable `GF_PATHS_DATA` is set, that will
take the precedence over the value configured in the provisioning file.

- `skipTlsCheck`: If Grafana instance is configured to use TLS with self signed certificates
set this parameter to `true` to skip TLS certificate check.
Expand Down

0 comments on commit c4ff5f8

Please sign in to comment.