Skip to content

Commit 6fd7aa8

Browse files
authored
test: ensure Prometheus queries can be rendered (#762)
1 parent 5f56563 commit 6fd7aa8

File tree

8 files changed

+314
-20
lines changed

8 files changed

+314
-20
lines changed

tests/acceptance/dockerinfra_test.go

Lines changed: 93 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package acceptance
22

33
import (
4-
"bytes"
5-
_ "embed"
4+
"embed"
5+
"fmt"
66
"io"
7+
"io/fs"
78
"os"
9+
"path"
810
"strings"
911
"testing"
1012
"time"
@@ -169,11 +171,8 @@ type Grafana struct {
169171
HTTPEndpoint string
170172
}
171173

172-
//go:embed fixtures/dashboards-provisioner.yaml
173-
var dashboardsProvisioner []byte
174-
175-
//go:embed fixtures/all-panels.json
176-
var allPanelsDashboard []byte
174+
//go:embed fixtures
175+
var fixturesFS embed.FS
177176

178177
func StartGrafana(tb testing.TB, options ...ContainerOption) *Grafana {
179178
tb.Helper()
@@ -188,21 +187,11 @@ func StartGrafana(tb testing.TB, options ...ContainerOption) *Grafana {
188187
Image: "docker.io/grafana/grafana-enterprise:main",
189188
WaitingFor: wait.ForAll(
190189
wait.ForHTTP("/healthz").WithPort(httpPort).WithAllowInsecure(true),
191-
wait.ForLog("finished to provision dashboards"), // from the provisioning files we add
190+
wait.ForLog("inserting datasource from configuration"), // from the provisioning files we add
191+
wait.ForLog("finished to provision dashboards"), // from the provisioning files we add
192192
),
193193
ExposedPorts: []string{"3000/tcp"},
194-
Files: []testcontainers.ContainerFile{
195-
{
196-
Reader: bytes.NewReader(dashboardsProvisioner),
197-
ContainerFilePath: "/etc/grafana/provisioning/dashboards/dashboards.yaml",
198-
FileMode: 0o777,
199-
},
200-
{
201-
Reader: bytes.NewReader(allPanelsDashboard),
202-
ContainerFilePath: "/usr/share/grafana/dashboards/all-panels.json",
203-
FileMode: 0o777,
204-
},
205-
},
194+
Files: createGrafanaProvisioningFiles(tb),
206195
},
207196
}
208197
for _, f := range options {
@@ -221,3 +210,87 @@ func StartGrafana(tb testing.TB, options ...ContainerOption) *Grafana {
221210
HTTPEndpoint: endpoint,
222211
}
223212
}
213+
214+
func createGrafanaProvisioningFiles(tb testing.TB) []testcontainers.ContainerFile {
215+
tb.Helper()
216+
217+
files, err := fsToContainerFiles(embedFSSub(tb, fixturesFS, "fixtures/dashboards"), ".", "/usr/share/grafana/dashboards")
218+
require.NoError(tb, err, "could not create container files from embedded dashboards")
219+
220+
provisioningFiles, err := fsToContainerFiles(embedFSSub(tb, fixturesFS, "fixtures/provisioning"), ".", "/etc/grafana/provisioning")
221+
require.NoError(tb, err, "could not create container files from embedded provisioning datasources")
222+
files = append(files, provisioningFiles...)
223+
224+
return files
225+
}
226+
227+
type embedFS interface {
228+
fs.FS
229+
fs.ReadDirFS
230+
fs.ReadFileFS
231+
}
232+
233+
func embedFSSub(tb testing.TB, f embedFS, dir string) embedFS {
234+
sub, err := fs.Sub(f, dir)
235+
require.NoError(tb, err, "could not get sub fs for dir %q", dir)
236+
if efs, ok := sub.(embedFS); ok {
237+
return efs
238+
}
239+
require.Fail(tb, "sub fs is not an embedFS", "got type %T", sub)
240+
panic("unreachable")
241+
}
242+
243+
func fsToContainerFiles(fs embedFS, baseSrc, baseDst string) ([]testcontainers.ContainerFile, error) {
244+
var files []testcontainers.ContainerFile
245+
246+
entries, err := fs.ReadDir(baseSrc)
247+
if err != nil {
248+
return nil, fmt.Errorf("failed to read embedded dir %q: %w", baseSrc, err)
249+
}
250+
for _, f := range entries {
251+
if f.IsDir() {
252+
subFiles, err := fsToContainerFiles(fs, path.Join(baseSrc, f.Name()), path.Join(baseDst, f.Name()))
253+
if err != nil {
254+
return nil, fmt.Errorf("failed to join with subdir %q: %w", f.Name(), err)
255+
}
256+
files = append(files, subFiles...)
257+
continue
258+
}
259+
260+
file, err := fs.Open(path.Join(baseSrc, f.Name()))
261+
if err != nil {
262+
return nil, fmt.Errorf("failed to read file %q: %w", path.Join(baseSrc, f.Name()), err)
263+
}
264+
265+
files = append(files, testcontainers.ContainerFile{
266+
Reader: file,
267+
ContainerFilePath: path.Join(baseDst, f.Name()),
268+
FileMode: 0o777,
269+
})
270+
}
271+
272+
return files, nil
273+
}
274+
275+
func StartPrometheus(tb testing.TB, options ...ContainerOption) {
276+
tb.Helper()
277+
278+
req := testcontainers.GenericContainerRequest{
279+
Logger: log.TestLogger(tb),
280+
Started: true,
281+
ContainerRequest: testcontainers.ContainerRequest{
282+
Image: "prom/prometheus:latest",
283+
WaitingFor: wait.ForAll(
284+
wait.ForHTTP("/-/healthy"),
285+
wait.ForLog("Server is ready"),
286+
),
287+
},
288+
}
289+
for _, f := range options {
290+
f(tb, &req)
291+
}
292+
293+
container, err := testcontainers.GenericContainer(tb.Context(), req)
294+
require.NoError(tb, err, "could not start service container?")
295+
testcontainers.CleanupContainer(tb, container)
296+
}
File renamed without changes.
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
{
2+
"annotations": {
3+
"list": [
4+
{
5+
"builtIn": 1,
6+
"datasource": {
7+
"type": "grafana",
8+
"uid": "-- Grafana --"
9+
},
10+
"enable": true,
11+
"hide": true,
12+
"iconColor": "rgba(0, 211, 255, 1)",
13+
"name": "Annotations & Alerts",
14+
"type": "dashboard"
15+
}
16+
]
17+
},
18+
"editable": true,
19+
"fiscalYearStartMonth": 0,
20+
"graphTooltip": 0,
21+
"id": 2,
22+
"links": [],
23+
"panels": [
24+
{
25+
"datasource": {
26+
"type": "prometheus",
27+
"uid": "${prom}"
28+
},
29+
"fieldConfig": {
30+
"defaults": {
31+
"color": {
32+
"mode": "palette-classic"
33+
},
34+
"custom": {
35+
"axisBorderShow": false,
36+
"axisCenteredZero": false,
37+
"axisColorMode": "text",
38+
"axisLabel": "",
39+
"axisPlacement": "auto",
40+
"barAlignment": 0,
41+
"barWidthFactor": 0.6,
42+
"drawStyle": "line",
43+
"fillOpacity": 0,
44+
"gradientMode": "none",
45+
"hideFrom": {
46+
"legend": false,
47+
"tooltip": false,
48+
"viz": false
49+
},
50+
"insertNulls": false,
51+
"lineInterpolation": "linear",
52+
"lineWidth": 1,
53+
"pointSize": 5,
54+
"scaleDistribution": {
55+
"type": "linear"
56+
},
57+
"showPoints": "auto",
58+
"spanNulls": false,
59+
"stacking": {
60+
"group": "A",
61+
"mode": "none"
62+
},
63+
"thresholdsStyle": {
64+
"mode": "off"
65+
}
66+
},
67+
"mappings": [],
68+
"thresholds": {
69+
"mode": "absolute",
70+
"steps": [
71+
{
72+
"color": "green",
73+
"value": 0
74+
},
75+
{
76+
"color": "red",
77+
"value": 80
78+
}
79+
]
80+
}
81+
},
82+
"overrides": []
83+
},
84+
"gridPos": {
85+
"h": 8,
86+
"w": 12,
87+
"x": 0,
88+
"y": 0
89+
},
90+
"id": 1,
91+
"options": {
92+
"legend": {
93+
"calcs": [],
94+
"displayMode": "list",
95+
"placement": "bottom",
96+
"showLegend": true
97+
},
98+
"tooltip": {
99+
"hideZeros": false,
100+
"mode": "single",
101+
"sort": "none"
102+
}
103+
},
104+
"pluginVersion": "12.2.0-92405",
105+
"targets": [
106+
{
107+
"datasource": {
108+
"type": "prometheus",
109+
"uid": "${prom}"
110+
},
111+
"editorMode": "code",
112+
"expr": "1",
113+
"legendFormat": "__auto",
114+
"range": true,
115+
"refId": "A"
116+
}
117+
],
118+
"title": "Prometheus: 1",
119+
"type": "timeseries"
120+
}
121+
],
122+
"preload": false,
123+
"schemaVersion": 41,
124+
"tags": [],
125+
"templating": {
126+
"list": [
127+
{
128+
"current": {
129+
"text": "Prometheus",
130+
"value": "prometheus"
131+
},
132+
"name": "prom",
133+
"options": [],
134+
"query": "prometheus",
135+
"refresh": 1,
136+
"regex": "",
137+
"type": "datasource"
138+
}
139+
]
140+
},
141+
"time": {
142+
"from": "now-6h",
143+
"to": "now"
144+
},
145+
"timepicker": {},
146+
"timezone": "browser",
147+
"title": "Provisioned Prometheus testing",
148+
"uid": "provisioned-prom-testing",
149+
"version": 2
150+
}

tests/acceptance/fixtures/dashboards-provisioner.yaml renamed to tests/acceptance/fixtures/provisioning/dashboards/dashboards-provisioner.yaml

File renamed without changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
apiVersion: 1
2+
3+
datasources:
4+
- name: Prometheus
5+
type: prometheus
6+
access: proxy
7+
url: http://prometheus:9090
8+
isDefault: true
9+
uid: prometheus
10+
editable: false
11+
jsonData:
12+
httpMethod: POST
13+
prometheusType: Prometheus
14+
prometheusVersion: '3.3.0'
15+
timeInterval: 10s
17.2 KB
Loading

tests/acceptance/rendering_grafana_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,60 @@ func TestRenderingGrafana(t *testing.T) {
8686
require.NoError(t, err, "could not update fixture file")
8787
}
8888
})
89+
90+
t.Run("render prometheus dashboard", func(t *testing.T) {
91+
t.Parallel()
92+
93+
net, err := network.New(t.Context())
94+
require.NoError(t, err, "could not create Docker network")
95+
testcontainers.CleanupNetwork(t, net)
96+
97+
StartPrometheus(t, WithNetwork(net, "prometheus"))
98+
svc := StartImageRenderer(t, WithNetwork(net, "gir"))
99+
_ = StartGrafana(t,
100+
WithNetwork(net, "grafana"),
101+
WithEnv("GF_FEATURE_TOGGLES_ENABLE", "renderAuthJWT"),
102+
WithEnv("GF_RENDERING_SERVER_URL", "http://gir:8081/render"),
103+
WithEnv("GF_RENDERING_CALLBACK_URL", "http://grafana:3000/"),
104+
WithEnv("GF_LOG_FILTERS", "debug"),
105+
WithEnv("GF_RENDERING_RENDERER_TOKEN", rendererAuthToken))
106+
107+
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, svc.HTTPEndpoint+"/render", nil)
108+
require.NoError(t, err, "could not construct HTTP request to Grafana")
109+
req.Header.Set("Accept", "image/png")
110+
req.Header.Set("X-Auth-Token", "-")
111+
query := req.URL.Query()
112+
query.Set("url", "http://grafana:3000/d/provisioned-prom-testing?render=1&from=1699333200000&to=1699344000000&kiosk=true")
113+
query.Set("encoding", "png")
114+
query.Set("width", "1400")
115+
query.Set("height", "800")
116+
query.Set("renderKey", renderKey)
117+
query.Set("domain", "grafana")
118+
req.URL.RawQuery = query.Encode()
119+
120+
resp, err := http.DefaultClient.Do(req)
121+
require.NoError(t, err, "could not send HTTP request to Grafana")
122+
require.Equal(t, http.StatusOK, resp.StatusCode, "unexpected HTTP status code from Grafana")
123+
124+
const fixturePath = "fixtures/render-prometheus.png"
125+
fixture, err := os.Open(fixturePath)
126+
require.NoError(t, err, "could not read fixture file")
127+
fixtureImg, err := png.Decode(fixture)
128+
require.NoError(t, err, "could not decode fixture PNG image")
129+
body, err := io.ReadAll(resp.Body)
130+
require.NoError(t, err, "could not read response body")
131+
bodyImg, err := png.Decode(bytes.NewReader(body))
132+
require.NoError(t, err, "could not decode response PNG image")
133+
134+
assert.Equal(t, bodyImg.Bounds().Max.X, 1400, "rendered image has wrong width")
135+
assert.Equal(t, bodyImg.Bounds().Max.Y, 800, "rendered image has wrong height")
136+
137+
diff, err := CountPixelDifferences(fixtureImg, bodyImg)
138+
const pixelThreshold = 15_000
139+
ok := assert.NoError(t, err, "could not diff images") && assert.LessOrEqual(t, diff, uint64(pixelThreshold), "rendered image has changed significantly")
140+
if !ok && os.Getenv("UPDATE_FIXTURES") == "true" {
141+
err := os.WriteFile(fixturePath, body, 0o644)
142+
require.NoError(t, err, "could not update fixture file")
143+
}
144+
})
89145
}

0 commit comments

Comments
 (0)