From ec74a721ea360c07356cc1d743b6118831a26cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Tue, 18 Nov 2025 14:01:57 +0100 Subject: [PATCH] Be more tolerant to invalid JSON when parsing dashboards for indexing (#114040) * Tags can only be string array. * Be more lenient when parsing dashboard. Parse what we can, don't error out easily. * Check element before parsing, log unexpected types. --- .../store/kind/dashboard/dashboard.go | 317 +++++++++++++----- .../store/kind/dashboard/dashboard_test.go | 2 + pkg/services/store/kind/dashboard/targets.go | 24 +- .../testdata/absolute-garbage-info.json | 58 ++++ .../dashboard/testdata/absolute-garbage.json | 84 +++++ .../k8s-wrapper-tags-string-info.json | 74 ++++ .../testdata/k8s-wrapper-tags-string.json | 122 +++++++ pkg/storage/unified/search/dashboard.go | 5 +- 8 files changed, 586 insertions(+), 100 deletions(-) create mode 100644 pkg/services/store/kind/dashboard/testdata/absolute-garbage-info.json create mode 100644 pkg/services/store/kind/dashboard/testdata/absolute-garbage.json create mode 100644 pkg/services/store/kind/dashboard/testdata/k8s-wrapper-tags-string-info.json create mode 100644 pkg/services/store/kind/dashboard/testdata/k8s-wrapper-tags-string.json diff --git a/pkg/services/store/kind/dashboard/dashboard.go b/pkg/services/store/kind/dashboard/dashboard.go index 848acb620e5..42a2e1e5067 100644 --- a/pkg/services/store/kind/dashboard/dashboard.go +++ b/pkg/services/store/kind/dashboard/dashboard.go @@ -1,16 +1,16 @@ package dashboard import ( + "errors" + "fmt" "io" "strconv" "strings" jsoniter "github.com/json-iterator/go" -) -func logf(format string, a ...any) { - //fmt.Printf(format, a...) -} + "github.com/grafana/grafana/pkg/infra/log" +) type templateVariable struct { current struct { @@ -48,7 +48,7 @@ func (d *datasourceVariableLookup) getDsRefsByTemplateVariableValue(value string case "No data sources found": return []DataSourceRef{} default: - // some variables use `ds.name` rather `ds.uid` + // some variables use `ds.name` rather than `ds.uid` if ref := d.dsLookup.ByRef(&DataSourceRef{ UID: value, }); ref != nil { @@ -114,45 +114,63 @@ func newDatasourceVariableLookup(dsLookup DatasourceLookup) *datasourceVariableL // ReadDashboard will take a byte stream and return dashboard info func ReadDashboard(stream io.Reader, lookup DatasourceLookup) (*DashboardSummaryInfo, error) { + return ReadDashboardWithLogContext(stream, lookup, nil) +} + +func ReadDashboardWithLogContext(stream io.Reader, lookup DatasourceLookup, logContext map[string]any) (*DashboardSummaryInfo, error) { iter := jsoniter.Parse(jsoniter.ConfigDefault, stream, 1024) - return readDashboardIter(iter, lookup) + return readDashboardIter("$", iter, lookup, logContext) } // nolint:gocyclo -func readDashboardIter(iter *jsoniter.Iterator, lookup DatasourceLookup) (*DashboardSummaryInfo, error) { +func readDashboardIter(jsonPath string, iter *jsoniter.Iterator, lookup DatasourceLookup, lc map[string]any) (*DashboardSummaryInfo, error) { dash := &DashboardSummaryInfo{} + if !checkAndSkipUnexpectedElement(iter, jsonPath, lc, jsoniter.ObjectValue) { + return dash, errors.New("expected JSON object at " + jsonPath) + } + datasourceVariablesLookup := newDatasourceVariableLookup(lookup) - for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() { + for field := iter.ReadObject(); field != ""; field = iter.ReadObject() { // Skip null values so we don't need special int handling if iter.WhatIsNext() == jsoniter.NilValue { iter.Skip() continue } - switch l1Field { + switch field { // k8s metadata wrappers (skip) case "metadata", "kind", "apiVersion": - _ = iter.Read() + iter.Skip() // recursively read the spec as dashboard json case "spec": - return readDashboardIter(iter, lookup) + return readDashboardIter(jsonPath+".spec", iter, lookup, lc) case "id": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".id", lc, jsoniter.NumberValue) { + continue + } dash.ID = iter.ReadInt64() - case "uid": - iter.ReadString() - case "title": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".title", lc, jsoniter.StringValue) { + continue + } dash.Title = iter.ReadString() case "description": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".description", lc, jsoniter.StringValue) { + continue + } dash.Description = iter.ReadString() case "schemaVersion": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".schemaVersion", lc, jsoniter.NumberValue, jsoniter.StringValue) { + continue + } + switch iter.WhatIsNext() { case jsoniter.NumberValue: dash.SchemaVersion = iter.ReadInt64() @@ -164,10 +182,18 @@ func readDashboardIter(iter *jsoniter.Iterator, lookup DatasourceLookup) (*Dashb default: iter.Skip() } + case "timezone": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".timezone", lc, jsoniter.StringValue) { + continue + } dash.TimeZone = iter.ReadString() case "editable": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".editable", lc, jsoniter.StringValue, jsoniter.BoolValue) { + continue + } + switch iter.WhatIsNext() { case jsoniter.BoolValue: dash.ReadOnly = !iter.ReadBool() @@ -178,25 +204,42 @@ func readDashboardIter(iter *jsoniter.Iterator, lookup DatasourceLookup) (*Dashb } case "refresh": - nxt := iter.WhatIsNext() - if nxt == jsoniter.StringValue { - dash.Refresh = iter.ReadString() - } else { - iter.Skip() + if !checkAndSkipUnexpectedElement(iter, jsonPath+".refresh", lc, jsoniter.StringValue) { + continue } + dash.Refresh = iter.ReadString() case "tags": - for iter.ReadArray() { + tagsPath := jsonPath + ".tags" + + // Only support string array tags. Ignore everything else. + if !checkAndSkipUnexpectedElement(iter, tagsPath, lc, jsoniter.ArrayValue) { + continue + } + + for ix := 0; iter.ReadArray(); ix++ { + if !checkAndSkipUnexpectedElement(iter, fmt.Sprintf("%s[%d]", tagsPath, ix), lc, jsoniter.StringValue) { + continue + } + dash.Tags = append(dash.Tags, iter.ReadString()) } case "links": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".links", lc, jsoniter.ArrayValue) { + continue + } + for iter.ReadArray() { iter.Skip() dash.LinkCount++ } case "time": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".time", lc, jsoniter.ObjectValue) { + continue + } + obj, ok := iter.Read().(map[string]any) if ok { if timeFrom, ok := obj["from"].(string); ok { @@ -206,61 +249,66 @@ func readDashboardIter(iter *jsoniter.Iterator, lookup DatasourceLookup) (*Dashb dash.TimeTo = timeTo } } + case "panels": - for iter.ReadArray() { - dash.Panels = append(dash.Panels, readpanelInfo(iter, lookup)) + panelsPath := jsonPath + ".panels" + if !checkAndSkipUnexpectedElement(iter, panelsPath, lc, jsoniter.ArrayValue) { + continue } - case "rows": - for iter.ReadArray() { - v := iter.Read() - logf("[DASHBOARD.ROW???] id=%s // %v\n", dash.ID, v) - } - - case "annotations": - switch iter.WhatIsNext() { - case jsoniter.ArrayValue: - // dashboards v2 is an array - for iter.ReadArray() { - v := iter.Read() - logf("[dash.anno] %v\n", v) + for ix := 0; iter.ReadArray(); ix++ { + p, ok := readpanelInfo(iter, lookup, fmt.Sprintf("%s[%d]", panelsPath, ix), lc) + if ok { + dash.Panels = append(dash.Panels, p) } - case jsoniter.ObjectValue: - // dashboards v0/v1 are an object - for sub := iter.ReadObject(); sub != ""; sub = iter.ReadObject() { - if sub == "list" { - for iter.ReadArray() { - v := iter.Read() - logf("[dash.anno] %v\n", v) - } - } else { - iter.Skip() - } - } - default: - iter.Skip() } case "templating": + templatingPath := jsonPath + ".templating" + if !checkAndSkipUnexpectedElement(iter, templatingPath, lc, jsoniter.ObjectValue) { + continue + } + for sub := iter.ReadObject(); sub != ""; sub = iter.ReadObject() { if sub == "list" { - for iter.ReadArray() { - templateVariable := templateVariable{} + templatingListPath := templatingPath + ".list" + if !checkAndSkipUnexpectedElement(iter, templatingListPath, lc, jsoniter.ArrayValue) { + continue + } + + for ix := 0; iter.ReadArray(); ix++ { + tv := templateVariable{} + + templatingListElementPath := fmt.Sprintf("%s[%d]", templatingListPath, ix) + if !checkAndSkipUnexpectedElement(iter, templatingListElementPath, lc, jsoniter.ObjectValue) { + continue + } for k := iter.ReadObject(); k != ""; k = iter.ReadObject() { switch k { case "name": + if !checkAndSkipUnexpectedElement(iter, templatingListElementPath+".name", lc, jsoniter.StringValue) { + continue + } + name := iter.ReadString() dash.TemplateVars = append(dash.TemplateVars, name) - templateVariable.name = name + tv.name = name case "type": - templateVariable.variableType = iter.ReadString() + if !checkAndSkipUnexpectedElement(iter, templatingListElementPath+".type", lc, jsoniter.StringValue) { + continue + } + tv.variableType = iter.ReadString() case "query": - templateVariable.query = iter.Read() + tv.query = iter.Read() case "current": + if !checkAndSkipUnexpectedElement(iter, templatingListElementPath+".current", lc, jsoniter.ObjectValue) { + continue + } + for c := iter.ReadObject(); c != ""; c = iter.ReadObject() { if c == "value" { - templateVariable.current.value = iter.Read() + tv.current.value = iter.Read() } else { iter.Skip() } @@ -270,8 +318,8 @@ func readDashboardIter(iter *jsoniter.Iterator, lookup DatasourceLookup) (*Dashb } } - if templateVariable.variableType == "datasource" { - datasourceVariablesLookup.add(templateVariable) + if tv.variableType == "datasource" { + datasourceVariablesLookup.add(tv) } } } else { @@ -279,17 +327,9 @@ func readDashboardIter(iter *jsoniter.Iterator, lookup DatasourceLookup) (*Dashb } } - // Ignore these properties - case "timepicker": - fallthrough - case "version": - fallthrough - case "iteration": - iter.Skip() - + // Ignore everything else default: - v := iter.Read() - logf("[DASHBOARD] support key: %s / %v\n", l1Field, v) + iter.Skip() } } @@ -311,6 +351,64 @@ func readDashboardIter(iter *jsoniter.Iterator, lookup DatasourceLookup) (*Dashb return dash, iter.Error } +var logger = log.New("services.store.kind.dashboard") + +// checkAndSkipUnexpectedElement verifies if the next JSON element matches any allowed value types for the specified JSON path. +// If the type matches, it returns true, otherwise it skips the element, logs an error, and returns false. +func checkAndSkipUnexpectedElement(iter *jsoniter.Iterator, jsonPath string, logContext map[string]any, allowedValues ...jsoniter.ValueType) bool { + next := iter.WhatIsNext() + for _, a := range allowedValues { + if next == a { + return true + } + } + + // Skip unexpected element. + iter.Skip() + + // Prepare log message. + params := []any{ + "jsonPath", jsonPath, + "got", valueTypesToString(next), + "expected", valueTypesToString(allowedValues...), + } + + // Map iteration is random, so a log message may look different each time if there are multiple entries in the map. That's fine. + for k, v := range logContext { + params = append(params, k, v) + } + + logger.Error("Unexpected element in Dashboard JSON", params...) + return false +} + +func valueTypesToString(allowedValues ...jsoniter.ValueType) string { + expected := strings.Builder{} + for ix, a := range allowedValues { + if ix > 0 { + expected.WriteString(", ") + } + + switch a { + case jsoniter.NilValue: + expected.WriteString("null") + case jsoniter.StringValue: + expected.WriteString("string") + case jsoniter.NumberValue: + expected.WriteString("number") + case jsoniter.BoolValue: + expected.WriteString("bool") + case jsoniter.ArrayValue: + expected.WriteString("array") + case jsoniter.ObjectValue: + expected.WriteString("object") + default: + expected.WriteString(fmt.Sprintf("unknown: %d", a)) + } + } + return expected.String() +} + func panelRequiresDatasource(panel PanelSummaryInfo) bool { return panel.Type != "row" } @@ -399,16 +497,20 @@ func findDatasourceRefsForVariables(dsVariableRefs []DataSourceRef, datasourceVa return referencedDs } -// will always return strings for now -func readpanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup) PanelSummaryInfo { +// nolint:gocyclo +func readpanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup, jsonPath string, lc map[string]any) (PanelSummaryInfo, bool) { panel := PanelSummaryInfo{} + if !checkAndSkipUnexpectedElement(iter, jsonPath, lc, jsoniter.ObjectValue) { + return panel, false + } + targets := newTargetInfo(lookup) - for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() { + for field := iter.ReadObject(); field != ""; field = iter.ReadObject() { if iter.WhatIsNext() == jsoniter.NilValue { - if l1Field == "datasource" { - targets.addDatasource(iter) + if field == "datasource" { + targets.addDatasource(iter, jsonPath+".datasource", lc) continue } @@ -417,23 +519,42 @@ func readpanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup) PanelSummar continue } - switch l1Field { + switch field { case "id": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".id", lc, jsoniter.NumberValue) { + continue + } panel.ID = iter.ReadInt64() case "type": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".type", lc, jsoniter.StringValue) { + continue + } panel.Type = iter.ReadString() case "title": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".title", lc, jsoniter.StringValue) { + continue + } panel.Title = iter.ReadString() case "description": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".description", lc, jsoniter.StringValue) { + continue + } panel.Description = iter.ReadString() case "pluginVersion": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".pluginVersion", lc, jsoniter.StringValue) { + continue + } panel.PluginVersion = iter.ReadString() // since 7x (the saved version for the plugin model) case "libraryPanel": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".libraryPanel", lc, jsoniter.ObjectValue) { + continue + } + var v map[string]interface{} iter.ReadVal(&v) if uid, ok := v["uid"]; ok { @@ -443,26 +564,42 @@ func readpanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup) PanelSummar } case "datasource": - targets.addDatasource(iter) + targets.addDatasource(iter, jsonPath+".datasource", lc) case "targets": + if !checkAndSkipUnexpectedElement(iter, jsonPath+".targets", lc, jsoniter.ArrayValue, jsoniter.ObjectValue) { + continue + } + switch iter.WhatIsNext() { case jsoniter.ArrayValue: - for iter.ReadArray() { - targets.addTarget(iter) + for ix := 0; iter.ReadArray(); ix++ { + targets.addTarget(iter, fmt.Sprintf("%s.targets[%d]", jsonPath, ix), lc) } case jsoniter.ObjectValue: - for f := iter.ReadObject(); f != ""; f = iter.ReadObject() { - targets.addTarget(iter) + for fn := iter.ReadObject(); fn != ""; fn = iter.ReadObject() { + targets.addTarget(iter, jsonPath+".targets."+fn, lc) } default: iter.Skip() } case "transformations": - for iter.ReadArray() { + if !checkAndSkipUnexpectedElement(iter, jsonPath+".transformations", lc, jsoniter.ArrayValue) { + continue + } + + for ix := 0; iter.ReadArray(); ix++ { + if !checkAndSkipUnexpectedElement(iter, fmt.Sprintf("%s.transformations[%d]", jsonPath, ix), lc, jsoniter.ObjectValue) { + continue + } + for sub := iter.ReadObject(); sub != ""; sub = iter.ReadObject() { if sub == "id" { + if !checkAndSkipUnexpectedElement(iter, fmt.Sprintf("%s.transformations[%d].id", jsonPath, ix), lc, jsoniter.StringValue) { + continue + } + panel.Transformer = append(panel.Transformer, iter.ReadString()) } else { iter.Skip() @@ -472,26 +609,26 @@ func readpanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup) PanelSummar // Rows have nested panels case "panels": - for iter.ReadArray() { - panel.Collapsed = append(panel.Collapsed, readpanelInfo(iter, lookup)) + if !checkAndSkipUnexpectedElement(iter, jsonPath+".panels", lc, jsoniter.ArrayValue) { + continue } - case "options": - fallthrough + for ix := 0; iter.ReadArray(); ix++ { + p, ok := readpanelInfo(iter, lookup, fmt.Sprintf("%s.panels[%d]", jsonPath, ix), lc) + if ok { + panel.Collapsed = append(panel.Collapsed, p) + } + } - case "gridPos": - fallthrough - - case "fieldConfig": + case "options", "gridPos", "fieldConfig": iter.Skip() default: - v := iter.Read() - logf("[PANEL] support key: %s / %v\n", l1Field, v) + iter.Skip() } } panel.Datasource = targets.GetDatasourceInfo() - return panel + return panel, true } diff --git a/pkg/services/store/kind/dashboard/dashboard_test.go b/pkg/services/store/kind/dashboard/dashboard_test.go index cdc336b55c1..5e31a86f8bd 100644 --- a/pkg/services/store/kind/dashboard/dashboard_test.go +++ b/pkg/services/store/kind/dashboard/dashboard_test.go @@ -55,6 +55,7 @@ func dsLookupForTests() DatasourceLookup { func TestReadDashboard(t *testing.T) { inputs := []string{ + "absolute-garbage", "check-string-datasource-id", "all-panels", "panel-graph/graph-shared-tooltips", @@ -73,6 +74,7 @@ func TestReadDashboard(t *testing.T) { "panel-with-library-panel-field", "k8s-wrapper", "k8s-wrapper-editable-string", + "k8s-wrapper-tags-string", } devdash := "../../../../../devenv/dev-dashboards/" diff --git a/pkg/services/store/kind/dashboard/targets.go b/pkg/services/store/kind/dashboard/targets.go index 603fec0b0d6..ef35329e424 100644 --- a/pkg/services/store/kind/dashboard/targets.go +++ b/pkg/services/store/kind/dashboard/targets.go @@ -27,7 +27,11 @@ func (s *targetInfo) GetDatasourceInfo() []DataSourceRef { } // the node will either be string (name|uid) OR ref -func (s *targetInfo) addDatasource(iter *jsoniter.Iterator) { +func (s *targetInfo) addDatasource(iter *jsoniter.Iterator, jsonPath string, lc map[string]any) { + if !checkAndSkipUnexpectedElement(iter, jsonPath, lc, jsoniter.StringValue, jsoniter.NilValue, jsoniter.ObjectValue) { + return + } + switch iter.WhatIsNext() { case jsoniter.StringValue: key := iter.ReadString() @@ -55,8 +59,7 @@ func (s *targetInfo) addDatasource(iter *jsoniter.Iterator) { } default: - v := iter.Read() - logf("[Panel.datasource.unknown] %v\n", v) + iter.Skip() } } @@ -66,18 +69,21 @@ func (s *targetInfo) addRef(ref *DataSourceRef) { } } -func (s *targetInfo) addTarget(iter *jsoniter.Iterator) { - for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() { - switch l1Field { +func (s *targetInfo) addTarget(iter *jsoniter.Iterator, jsonPath string, lc map[string]any) { + if !checkAndSkipUnexpectedElement(iter, jsonPath, lc, jsoniter.ObjectValue) { + return + } + + for f := iter.ReadObject(); f != ""; f = iter.ReadObject() { + switch f { case "datasource": - s.addDatasource(iter) + s.addDatasource(iter, jsonPath+".datasource", lc) case "refId": iter.Skip() default: - v := iter.Read() - logf("[Panel.TARGET] %s=%v\n", l1Field, v) + iter.Skip() } } } diff --git a/pkg/services/store/kind/dashboard/testdata/absolute-garbage-info.json b/pkg/services/store/kind/dashboard/testdata/absolute-garbage-info.json new file mode 100644 index 00000000000..8d74369dadf --- /dev/null +++ b/pkg/services/store/kind/dashboard/testdata/absolute-garbage-info.json @@ -0,0 +1,58 @@ +{ + "title": "adfbg6f", + "tags": null, + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ], + "panels": [ + { + "id": 1, + "title": "green pie", + "libraryPanel": "a7975b7a-fb53-4ab7-951d-15810953b54f", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + }, + { + "id": 0, + "title": "", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + }, + { + "id": 7, + "title": "", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + }, + { + "id": 8, + "title": "", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + } + ], + "schemaVersion": 0, + "linkCount": 4, + "timeFrom": "", + "timeTo": "", + "timezone": "" +} \ No newline at end of file diff --git a/pkg/services/store/kind/dashboard/testdata/absolute-garbage.json b/pkg/services/store/kind/dashboard/testdata/absolute-garbage.json new file mode 100644 index 00000000000..3af33590cc9 --- /dev/null +++ b/pkg/services/store/kind/dashboard/testdata/absolute-garbage.json @@ -0,0 +1,84 @@ +{ + "metadata": 123, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": "true", + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": "not a number", + "links": [ + "http://12345", + { + "object": "goes, here" + }, + null, + true + ], + "liveNow": false, + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "libraryPanel": { + "name": "green pie", + "uid": "a7975b7a-fb53-4ab7-951d-15810953b54f" + }, + "title": "green pie" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": "not a number", + "title": true + }, + { + "id": 7, + "type": null + }, + { + "id": 8, + "type": { + "a": 123 + } + } + ], + "refresh": "", + "schemaVersion": null, + "tags": 1234, + "templating": { + "list": [] + }, + "time": false, + "timepicker": {}, + "timezone": 3.141592653589793, + "title": "adfbg6f", + "uid": ["aaa", "bbb", "cc"], + "description": [null, true], + "version": [], + "weekStart": false + } +} diff --git a/pkg/services/store/kind/dashboard/testdata/k8s-wrapper-tags-string-info.json b/pkg/services/store/kind/dashboard/testdata/k8s-wrapper-tags-string-info.json new file mode 100644 index 00000000000..6451c3d805b --- /dev/null +++ b/pkg/services/store/kind/dashboard/testdata/k8s-wrapper-tags-string-info.json @@ -0,0 +1,74 @@ +{ + "id": 141, + "title": "pppp", + "tags": null, + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ], + "panels": [ + { + "id": 1, + "title": "green pie", + "libraryPanel": "a7975b7a-fb53-4ab7-951d-15810953b54f", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + }, + { + "id": 2, + "title": "green pie", + "libraryPanel": "e1d5f519-dabd-47c6-9ad7-83d181ce1cee", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + }, + { + "id": 7, + "title": "", + "type": "barchart", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + }, + { + "id": 8, + "title": "", + "type": "graph", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + }, + { + "id": 3, + "title": "collapsed row", + "type": "row", + "collapsed": [ + { + "id": 42, + "title": "blue pie", + "libraryPanel": "l3d2s634-fdgf-75u4-3fg3-67j966ii7jur" + } + ] + } + ], + "schemaVersion": 38, + "linkCount": 0, + "timeFrom": "now-6h", + "timeTo": "now", + "timezone": "" +} \ No newline at end of file diff --git a/pkg/services/store/kind/dashboard/testdata/k8s-wrapper-tags-string.json b/pkg/services/store/kind/dashboard/testdata/k8s-wrapper-tags-string.json new file mode 100644 index 00000000000..28bca5c0079 --- /dev/null +++ b/pkg/services/store/kind/dashboard/testdata/k8s-wrapper-tags-string.json @@ -0,0 +1,122 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "adfbg6f", + "namespace": "default", + "uid": "b396894e-56bf-4a01-837b-64157912ca00", + "creationTimestamp": "2024-10-30T18:30:54Z", + "annotations": { + "grafana.app/createdBy": "user:be2g71ke8yoe8b", + "grafana.app/originHash": "Grafana v9.2.0 (NA)", + "grafana.app/originName": "UI", + "grafana.app/originPath": "/dashboard/new" + } + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 141, + "links": [], + "liveNow": false, + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "libraryPanel": { + "name": "green pie", + "uid": "a7975b7a-fb53-4ab7-951d-15810953b54f" + }, + "title": "green pie" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "libraryPanel": { + "name": "red pie", + "uid": "e1d5f519-dabd-47c6-9ad7-83d181ce1cee" + }, + "title": "green pie" + }, + { + "id": 7, + "type": "barchart" + }, + { + "id": 8, + "type": "graph" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 3, + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 42, + "libraryPanel": { + "name": "blue pie", + "uid": "l3d2s634-fdgf-75u4-3fg3-67j966ii7jur" + }, + "title": "blue pie" + } + ], + "title": "collapsed row", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 38, + "tags": "tag1,tag2, tag3,,", + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "pppp", + "uid": "adfbg6f", + "version": 3, + "weekStart": "" + } +} diff --git a/pkg/storage/unified/search/dashboard.go b/pkg/storage/unified/search/dashboard.go index 7e2284fb118..44a822a1b4c 100644 --- a/pkg/storage/unified/search/dashboard.go +++ b/pkg/storage/unified/search/dashboard.go @@ -261,7 +261,10 @@ func (s *DashboardDocumentBuilder) BuildDocument(ctx context.Context, key *resou value = rsp.Value } - summary, err := dashboard.ReadDashboard(bytes.NewReader(value), s.DatasourceLookup) + summary, err := dashboard.ReadDashboardWithLogContext(bytes.NewReader(value), s.DatasourceLookup, map[string]any{ + "document": fmt.Sprintf("%s/%s/%s/%s", key.GetNamespace(), key.GetGroup(), key.GetResource(), key.GetName()), + "rv": rv, + }) if err != nil { return nil, err }