fix: special handling of unmarshalling for invalid json dashboards (#108519)
* implement special dashboard fallback logic when dashboard data has invalid json behind feature flag --------- Co-authored-by: Will Assis <william@williamassis.com>
This commit is contained in:
co-authored by
Will Assis
parent
4a34a2313c
commit
98e37f2ca9
@@ -302,6 +302,10 @@ export interface FeatureToggles {
|
|||||||
*/
|
*/
|
||||||
dashboardSchemaValidationLogging?: boolean;
|
dashboardSchemaValidationLogging?: boolean;
|
||||||
/**
|
/**
|
||||||
|
* Enable fallback parsing behavior when scan row encounters invalid dashboard JSON
|
||||||
|
*/
|
||||||
|
scanRowInvalidDashboardParseFallbackEnabled?: boolean;
|
||||||
|
/**
|
||||||
* Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)
|
* Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)
|
||||||
*/
|
*/
|
||||||
datasourceQueryTypes?: boolean;
|
datasourceQueryTypes?: boolean;
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
featureManager, err := featuremgmt.ProvideManagerService(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
featureToggles := featuremgmt.ProvideToggles(featureManager)
|
||||||
|
|
||||||
provisioning, err := newStubProvisioning(cfg.ProvisioningPath)
|
provisioning, err := newStubProvisioning(cfg.ProvisioningPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -76,9 +82,10 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err
|
|||||||
nil, // no librarypanels.Service
|
nil, // no librarypanels.Service
|
||||||
sort.ProvideService(),
|
sort.ProvideService(),
|
||||||
acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
|
acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
|
||||||
|
featureToggles,
|
||||||
)
|
)
|
||||||
|
|
||||||
client, err := newUnifiedClient(cfg, sqlStore)
|
client, err := newUnifiedClient(cfg, sqlStore, featureToggles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -215,12 +222,7 @@ func promptYesNo(prompt string) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUnifiedClient(cfg *setting.Cfg, sqlStore db.DB) (resource.ResourceClient, error) {
|
func newUnifiedClient(cfg *setting.Cfg, sqlStore db.DB, featureToggles featuremgmt.FeatureToggles) (resource.ResourceClient, error) {
|
||||||
featureManager, err := featuremgmt.ProvideManagerService(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
featureToggles := featuremgmt.ProvideToggles(featureManager)
|
|
||||||
return unified.ProvideUnifiedStorageClient(&unified.Options{
|
return unified.ProvideUnifiedStorageClient(&unified.Options{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Features: featureToggles,
|
Features: featureToggles,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||||
@@ -51,9 +52,10 @@ func ProvideLegacyMigrator(
|
|||||||
provisioning provisioning.ProvisioningService, // only needed for dashboard settings
|
provisioning provisioning.ProvisioningService, // only needed for dashboard settings
|
||||||
libraryPanelSvc librarypanels.Service,
|
libraryPanelSvc librarypanels.Service,
|
||||||
accessControl accesscontrol.AccessControl,
|
accessControl accesscontrol.AccessControl,
|
||||||
|
features featuremgmt.FeatureToggles,
|
||||||
) LegacyMigrator {
|
) LegacyMigrator {
|
||||||
dbp := legacysql.NewDatabaseProvider(sql)
|
dbp := legacysql.NewDatabaseProvider(sql)
|
||||||
return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, libraryPanelSvc, sort.ProvideService(), accessControl)
|
return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, libraryPanelSvc, sort.ProvideService(), accessControl, features)
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlobStoreInfo struct {
|
type BlobStoreInfo struct {
|
||||||
@@ -309,11 +311,12 @@ func (a *dashboardSqlAccess) migrateDashboards(ctx context.Context, orgId int64,
|
|||||||
for _, row := range rows.rejected {
|
for _, row := range rows.rejected {
|
||||||
id := row.Dash.Labels[utils.LabelKeyDeprecatedInternalID]
|
id := row.Dash.Labels[utils.LabelKeyDeprecatedInternalID]
|
||||||
a.log.Warn("rejected dashboard",
|
a.log.Warn("rejected dashboard",
|
||||||
|
"namespace", opts.Namespace,
|
||||||
"dashboard", row.Dash.Name,
|
"dashboard", row.Dash.Name,
|
||||||
"uid", row.Dash.UID,
|
"uid", row.Dash.UID,
|
||||||
"id", id,
|
"id", id,
|
||||||
|
"version", row.Dash.Generation,
|
||||||
"stackId", opts.StackID,
|
"stackId", opts.StackID,
|
||||||
"namespace", opts.Namespace,
|
|
||||||
)
|
)
|
||||||
opts.Progress(-2, fmt.Sprintf("rejected: id:%s, uid:%s", id, row.Dash.Name))
|
opts.Progress(-2, fmt.Sprintf("rejected: id:%s, uid:%s", id, row.Dash.Name))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||||
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
|
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||||
@@ -62,6 +63,8 @@ type dashboardSqlAccess struct {
|
|||||||
namespacer request.NamespaceMapper
|
namespacer request.NamespaceMapper
|
||||||
provisioning provisioning.ProvisioningService
|
provisioning provisioning.ProvisioningService
|
||||||
|
|
||||||
|
invalidDashboardParseFallbackEnabled bool
|
||||||
|
|
||||||
// Use for writing (not reading)
|
// Use for writing (not reading)
|
||||||
dashStore dashboards.Store
|
dashStore dashboards.Store
|
||||||
dashboardSearchClient legacysearcher.DashboardSearchClient
|
dashboardSearchClient legacysearcher.DashboardSearchClient
|
||||||
@@ -82,17 +85,19 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider,
|
|||||||
libraryPanelSvc librarypanels.Service,
|
libraryPanelSvc librarypanels.Service,
|
||||||
sorter sort.Service,
|
sorter sort.Service,
|
||||||
accessControl accesscontrol.AccessControl,
|
accessControl accesscontrol.AccessControl,
|
||||||
|
features featuremgmt.FeatureToggles,
|
||||||
) DashboardAccess {
|
) DashboardAccess {
|
||||||
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
|
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
|
||||||
return &dashboardSqlAccess{
|
return &dashboardSqlAccess{
|
||||||
sql: sql,
|
sql: sql,
|
||||||
namespacer: namespacer,
|
namespacer: namespacer,
|
||||||
dashStore: dashStore,
|
dashStore: dashStore,
|
||||||
provisioning: provisioning,
|
provisioning: provisioning,
|
||||||
dashboardSearchClient: *dashboardSearchClient,
|
dashboardSearchClient: *dashboardSearchClient,
|
||||||
libraryPanelSvc: libraryPanelSvc,
|
libraryPanelSvc: libraryPanelSvc,
|
||||||
accessControl: accessControl,
|
accessControl: accessControl,
|
||||||
log: log.New("dashboard.legacysql"),
|
log: log.New("dashboard.legacysql"),
|
||||||
|
invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +181,7 @@ func (r *rowsWrapper) Next() bool {
|
|||||||
r.row, err = r.a.scanRow(r.rows, r.history)
|
r.row, err = r.a.scanRow(r.rows, r.history)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.a.log.Error("error scanning dashboard", "error", err)
|
r.a.log.Error("error scanning dashboard", "error", err)
|
||||||
if len(r.rejected) > 0 || r.row == nil {
|
if len(r.rejected) > 100 || r.row == nil {
|
||||||
r.err = fmt.Errorf("too many rejected rows (%d) %w", len(r.rejected), err)
|
r.err = fmt.Errorf("too many rejected rows (%d) %w", len(r.rejected), err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -228,6 +233,51 @@ func (r *rowsWrapper) Value() []byte {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateFallbackDashboard(data []byte, title, uid string) ([]byte, error) {
|
||||||
|
generatedDashboard := map[string]interface{}{
|
||||||
|
"editable": true,
|
||||||
|
"id": 1,
|
||||||
|
"panels": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"description": "The JSON is invalid. You can import it again after fixing it.",
|
||||||
|
"gridPos": map[string]interface{}{"h": 8, "w": 24, "x": 0, "y": 0},
|
||||||
|
"id": 1,
|
||||||
|
"options": map[string]interface{}{
|
||||||
|
"code": map[string]interface{}{"language": "plaintext", "showLineNumbers": false, "showMiniMap": false},
|
||||||
|
"content": string(data),
|
||||||
|
"mode": "code",
|
||||||
|
},
|
||||||
|
"title": "Invalid dashboard",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"schemaVersion": 41,
|
||||||
|
"title": title,
|
||||||
|
"uid": uid,
|
||||||
|
"version": 3,
|
||||||
|
}
|
||||||
|
return json.Marshal(generatedDashboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *dashboardSqlAccess) parseDashboard(dash *dashboardV1.Dashboard, data []byte, id int64, title string) error {
|
||||||
|
if err := dash.Spec.UnmarshalJSON(data); err != nil {
|
||||||
|
a.log.Warn("error unmarshalling dashboard spec. Generating fallback dashboard data", "error", err, "uid", dash.UID, "name", dash.Name)
|
||||||
|
dash.Spec = *dashboardV0.NewDashboardSpec()
|
||||||
|
|
||||||
|
dashboardData, err := generateFallbackDashboard(data, title, string(dash.UID))
|
||||||
|
if err != nil {
|
||||||
|
a.log.Warn("error generating fallback dashboard data", "error", err, "uid", dash.UID, "name", dash.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = dash.Spec.UnmarshalJSON(dashboardData); err != nil {
|
||||||
|
a.log.Warn("error unmarshalling fallback dashboard data", "error", err, "uid", dash.UID, "name", dash.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRow, error) {
|
func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRow, error) {
|
||||||
dash := &dashboardV1.Dashboard{
|
dash := &dashboardV1.Dashboard{
|
||||||
TypeMeta: dashboardV1.DashboardResourceInfo.TypeMeta(),
|
TypeMeta: dashboardV1.DashboardResourceInfo.TypeMeta(),
|
||||||
@@ -238,6 +288,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
|
|||||||
var dashboard_id int64
|
var dashboard_id int64
|
||||||
var orgId int64
|
var orgId int64
|
||||||
var folder_uid sql.NullString
|
var folder_uid sql.NullString
|
||||||
|
var title string
|
||||||
var updated time.Time
|
var updated time.Time
|
||||||
var updatedBy sql.NullString
|
var updatedBy sql.NullString
|
||||||
var updatedByID sql.NullInt64
|
var updatedByID sql.NullInt64
|
||||||
@@ -257,7 +308,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
|
|||||||
var data []byte // the dashboard JSON
|
var data []byte // the dashboard JSON
|
||||||
var version int64
|
var version int64
|
||||||
|
|
||||||
err := rows.Scan(&orgId, &dashboard_id, &dash.Name, &folder_uid,
|
err := rows.Scan(&orgId, &dashboard_id, &dash.Name, &title, &folder_uid,
|
||||||
&deleted, &plugin_id,
|
&deleted, &plugin_id,
|
||||||
&origin_name, &origin_path, &origin_hash, &origin_ts,
|
&origin_name, &origin_path, &origin_hash, &origin_ts,
|
||||||
&created, &createdBy, &createdByID,
|
&created, &createdBy, &createdByID,
|
||||||
@@ -286,6 +337,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
|
|||||||
dash.SetCreationTimestamp(metav1.NewTime(created))
|
dash.SetCreationTimestamp(metav1.NewTime(created))
|
||||||
meta, err := utils.MetaAccessor(dash)
|
meta, err := utils.MetaAccessor(dash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
a.log.Debug("failed to get meta accessor for dashboard", "error", err, "uid", dash.UID, "name", dash.Name, "version", version)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
meta.SetUpdatedTimestamp(&updated)
|
meta.SetUpdatedTimestamp(&updated)
|
||||||
@@ -331,9 +383,14 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(data) > 0 {
|
if len(data) > 0 {
|
||||||
err = dash.Spec.UnmarshalJSON(data)
|
if a.invalidDashboardParseFallbackEnabled {
|
||||||
if err != nil {
|
if err := a.parseDashboard(dash, data, dashboard_id, title); err != nil {
|
||||||
return row, fmt.Errorf("JSON unmarshal error for: %s // %w", dash.Name, err)
|
return row, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := dash.Spec.UnmarshalJSON(data); err != nil {
|
||||||
|
return row, fmt.Errorf("JSON unmarshal error for: %s // %w", dash.Name, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ignore any saved values for id/version/uid
|
// Ignore any saved values for id/version/uid
|
||||||
|
|||||||
@@ -32,13 +32,15 @@ func TestScanRow(t *testing.T) {
|
|||||||
provisioner := provisioning.NewProvisioningServiceMock(context.Background())
|
provisioner := provisioning.NewProvisioningServiceMock(context.Background())
|
||||||
provisioner.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "provisioner" }
|
provisioner.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "provisioner" }
|
||||||
store := &dashboardSqlAccess{
|
store := &dashboardSqlAccess{
|
||||||
namespacer: func(_ int64) string { return "default" },
|
namespacer: func(_ int64) string { return "default" },
|
||||||
provisioning: provisioner,
|
provisioning: provisioner,
|
||||||
log: log.New("test"),
|
log: log.New("test"),
|
||||||
|
invalidDashboardParseFallbackEnabled: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
columns := []string{"orgId", "dashboard_id", "name", "folder_uid", "deleted", "plugin_id", "origin_name", "origin_path", "origin_hash", "origin_ts", "created", "createdBy", "createdByID", "updated", "updatedBy", "updatedByID", "version", "message", "data", "api_version"}
|
columns := []string{"orgId", "dashboard_id", "name", "title", "folder_uid", "deleted", "plugin_id", "origin_name", "origin_path", "origin_hash", "origin_ts", "created", "createdBy", "createdByID", "updated", "updatedBy", "updatedByID", "version", "message", "data", "api_version"}
|
||||||
id := int64(100)
|
id := int64(100)
|
||||||
|
uid := "someuid"
|
||||||
title := "Test Dashboard"
|
title := "Test Dashboard"
|
||||||
folderUID := "folder123"
|
folderUID := "folder123"
|
||||||
timestamp := time.Now()
|
timestamp := time.Now()
|
||||||
@@ -49,7 +51,7 @@ func TestScanRow(t *testing.T) {
|
|||||||
updatedUser := "updator"
|
updatedUser := "updator"
|
||||||
|
|
||||||
t.Run("Should scan a valid row correctly", func(t *testing.T) {
|
t.Run("Should scan a valid row correctly", func(t *testing.T) {
|
||||||
rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
|
rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
|
||||||
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
||||||
resultRows, err := mockDB.Query("SELECT *")
|
resultRows, err := mockDB.Query("SELECT *")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -59,7 +61,7 @@ func TestScanRow(t *testing.T) {
|
|||||||
row, err := store.scanRow(resultRows, false)
|
row, err := store.scanRow(resultRows, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, row)
|
require.NotNil(t, row)
|
||||||
require.Equal(t, "Test Dashboard", row.Dash.Name)
|
require.Equal(t, uid, row.Dash.Name)
|
||||||
require.Equal(t, version, row.RV) // rv should be the dashboard version
|
require.Equal(t, version, row.RV) // rv should be the dashboard version
|
||||||
require.Equal(t, common.Unstructured{
|
require.Equal(t, common.Unstructured{
|
||||||
Object: map[string]interface{}{"key": "value"},
|
Object: map[string]interface{}{"key": "value"},
|
||||||
@@ -80,7 +82,7 @@ func TestScanRow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("File provisioned dashboard should have annotations", func(t *testing.T) {
|
t.Run("File provisioned dashboard should have annotations", func(t *testing.T) {
|
||||||
rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "", "provisioner", pathToFile, "hashing", 100000, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
|
rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "provisioner", pathToFile, "hashing", 100000, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
|
||||||
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
||||||
resultRows, err := mockDB.Query("SELECT *")
|
resultRows, err := mockDB.Query("SELECT *")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -108,7 +110,7 @@ func TestScanRow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Plugin provisioned dashboard should have annotations", func(t *testing.T) {
|
t.Run("Plugin provisioned dashboard should have annotations", func(t *testing.T) {
|
||||||
rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "slo", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
|
rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "slo", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
|
||||||
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
||||||
resultRows, err := mockDB.Query("SELECT *")
|
resultRows, err := mockDB.Query("SELECT *")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -144,7 +146,7 @@ func TestScanRow(t *testing.T) {
|
|||||||
// In migration scenario, COALESCE functions return dashboard table values
|
// In migration scenario, COALESCE functions return dashboard table values
|
||||||
// when dashboard_version values are NULL, ensuring all dashboards are migrated
|
// when dashboard_version values are NULL, ensuring all dashboards are migrated
|
||||||
rows := sqlmock.NewRows(columns).AddRow(
|
rows := sqlmock.NewRows(columns).AddRow(
|
||||||
1, id, title, folderUID, nil, "", // basic dashboard fields
|
1, id, uid, title, folderUID, nil, "", // basic dashboard fields
|
||||||
"", "", "", 0, // origin fields
|
"", "", "", 0, // origin fields
|
||||||
timestamp, createdUser, 0, // created fields
|
timestamp, createdUser, 0, // created fields
|
||||||
// These represent COALESCED values from dashboard table (not version table)
|
// These represent COALESCED values from dashboard table (not version table)
|
||||||
@@ -163,7 +165,8 @@ func TestScanRow(t *testing.T) {
|
|||||||
require.NotNil(t, row)
|
require.NotNil(t, row)
|
||||||
|
|
||||||
// Verify migration scenario works correctly with fallback data
|
// Verify migration scenario works correctly with fallback data
|
||||||
require.Equal(t, title, row.Dash.Name)
|
require.Equal(t, uid, row.Dash.Name)
|
||||||
|
require.Equal(t, "Migrated Dashboard", row.Dash.Spec.Object["title"])
|
||||||
require.Equal(t, migrationVersion, row.RV) // Should use COALESCEd dashboard table version
|
require.Equal(t, migrationVersion, row.RV) // Should use COALESCEd dashboard table version
|
||||||
require.Equal(t, common.Unstructured{
|
require.Equal(t, common.Unstructured{
|
||||||
Object: map[string]interface{}{
|
Object: map[string]interface{}{
|
||||||
@@ -187,6 +190,72 @@ func TestScanRow(t *testing.T) {
|
|||||||
require.Equal(t, folderUID, meta.GetFolder())
|
require.Equal(t, folderUID, meta.GetFolder())
|
||||||
require.Equal(t, "dashboard.grafana.app/"+migrationAPIVersion, row.Dash.APIVersion)
|
require.Equal(t, "dashboard.grafana.app/"+migrationAPIVersion, row.Dash.APIVersion)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should follow dashboard template when failing to unmarshal dashboard if feature flag X is enabled", func(t *testing.T) {
|
||||||
|
// row with bad data
|
||||||
|
badData := []byte(`{"rows":[{"panels":[{"targets":[{"refId":"A","target":"aliasSub(alias, '^(.{27}).+', '\1...')"}]}]}]}`)
|
||||||
|
rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, badData, "vXyz")
|
||||||
|
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
||||||
|
resultRows, err := mockDB.Query("SELECT *")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resultRows.Close() // nolint:errcheck
|
||||||
|
resultRows.Next()
|
||||||
|
|
||||||
|
row, err := store.scanRow(resultRows, false)
|
||||||
|
require.Error(t, err, "JSON unmarshal error for: Test Dashboard // invalid character '1' in string escape code")
|
||||||
|
require.NotNil(t, row)
|
||||||
|
// correctly scans these
|
||||||
|
require.Equal(t, uid, row.Dash.Name)
|
||||||
|
require.Equal(t, version, row.RV)
|
||||||
|
require.Equal(t, "default", row.Dash.Namespace)
|
||||||
|
require.Equal(t, &continueToken{orgId: int64(1), id: id}, row.token)
|
||||||
|
|
||||||
|
// failure case: does NOT parse the dashboard itself
|
||||||
|
require.Equal(t, common.Unstructured{
|
||||||
|
Object: nil,
|
||||||
|
}, row.Dash.Spec)
|
||||||
|
|
||||||
|
// store with feature flag enabled
|
||||||
|
store = &dashboardSqlAccess{
|
||||||
|
namespacer: func(_ int64) string { return "default" },
|
||||||
|
provisioning: provisioner,
|
||||||
|
log: log.New("test"),
|
||||||
|
invalidDashboardParseFallbackEnabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
row, err = store.scanRow(resultRows, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, row)
|
||||||
|
require.Equal(t, uid, row.Dash.Name)
|
||||||
|
require.Equal(t, version, row.RV)
|
||||||
|
require.Equal(t, "default", row.Dash.Namespace)
|
||||||
|
require.Equal(t, &continueToken{orgId: int64(1), id: id}, row.token)
|
||||||
|
|
||||||
|
// instead of failing, create dummy dashboard with broken json inlined in text panel
|
||||||
|
require.Equal(t, title, row.Dash.Spec.Object["title"])
|
||||||
|
panels, exists := row.Dash.Spec.Object["panels"]
|
||||||
|
require.True(t, exists, "panels property should exist")
|
||||||
|
|
||||||
|
panelsSlice, ok := panels.([]interface{})
|
||||||
|
require.True(t, ok, "panels should be a slice")
|
||||||
|
require.Len(t, panelsSlice, 1, "panels should have exactly one element")
|
||||||
|
|
||||||
|
panel, ok := panelsSlice[0].(map[string]interface{})
|
||||||
|
require.True(t, ok, "panel should be a map")
|
||||||
|
|
||||||
|
options, exists := panel["options"]
|
||||||
|
require.True(t, exists, "panel should have options property")
|
||||||
|
|
||||||
|
optionsMap, ok := options.(map[string]interface{})
|
||||||
|
require.True(t, ok, "options should be a map")
|
||||||
|
|
||||||
|
content, exists := optionsMap["content"]
|
||||||
|
require.True(t, exists, "options should have content property")
|
||||||
|
|
||||||
|
contentStr, ok := content.(string)
|
||||||
|
require.True(t, ok, "content should be a string")
|
||||||
|
require.Equal(t, string(badData), contentStr, "content should match bad json data")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildSaveDashboardCommand(t *testing.T) {
|
func TestBuildSaveDashboardCommand(t *testing.T) {
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@ SELECT
|
|||||||
dashboard.org_id,
|
dashboard.org_id,
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
dashboard.uid,
|
dashboard.uid,
|
||||||
|
dashboard.title,
|
||||||
dashboard.folder_uid,
|
dashboard.folder_uid,
|
||||||
dashboard.deleted,
|
dashboard.deleted,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ func RegisterAPIService(
|
|||||||
folderClient: folderClient,
|
folderClient: folderClient,
|
||||||
|
|
||||||
legacy: &DashboardStorage{
|
legacy: &DashboardStorage{
|
||||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, accessControl),
|
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, accessControl, features),
|
||||||
DashboardService: dashboardService,
|
DashboardService: dashboardService,
|
||||||
},
|
},
|
||||||
reg: reg,
|
reg: reg,
|
||||||
|
|||||||
@@ -746,7 +746,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser
|
|||||||
}
|
}
|
||||||
userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer)
|
userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer)
|
||||||
factory := github.ProvideFactory()
|
factory := github.ProvideFactory()
|
||||||
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl)
|
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl, featureToggles)
|
||||||
databaseDatabase := database5.ProvideDatabase(sqlStore, tracer)
|
databaseDatabase := database5.ProvideDatabase(sqlStore, tracer)
|
||||||
secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, registerer)
|
secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, registerer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1307,7 +1307,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface {
|
|||||||
}
|
}
|
||||||
userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer)
|
userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer)
|
||||||
factory := github.ProvideFactory()
|
factory := github.ProvideFactory()
|
||||||
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl)
|
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl, featureToggles)
|
||||||
databaseDatabase := database5.ProvideDatabase(sqlStore, tracer)
|
databaseDatabase := database5.ProvideDatabase(sqlStore, tracer)
|
||||||
secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, registerer)
|
secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, registerer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -501,6 +501,12 @@ var (
|
|||||||
Stage: FeatureStageExperimental,
|
Stage: FeatureStageExperimental,
|
||||||
Owner: grafanaAppPlatformSquad,
|
Owner: grafanaAppPlatformSquad,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "scanRowInvalidDashboardParseFallbackEnabled",
|
||||||
|
Description: "Enable fallback parsing behavior when scan row encounters invalid dashboard JSON",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
Owner: grafanaSearchAndStorageSquad,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "datasourceQueryTypes",
|
Name: "datasourceQueryTypes",
|
||||||
Description: "Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)",
|
Description: "Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)",
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,fals
|
|||||||
dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||||
dashboardDisableSchemaValidationV2,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
dashboardDisableSchemaValidationV2,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||||
dashboardSchemaValidationLogging,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
dashboardSchemaValidationLogging,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||||
|
scanRowInvalidDashboardParseFallbackEnabled,experimental,@grafana/search-and-storage,false,false,false
|
||||||
datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||||
queryService,experimental,@grafana/grafana-datasources-core-services,false,true,false
|
queryService,experimental,@grafana/grafana-datasources-core-services,false,true,false
|
||||||
queryServiceRewrite,experimental,@grafana/grafana-datasources-core-services,false,true,false
|
queryServiceRewrite,experimental,@grafana/grafana-datasources-core-services,false,true,false
|
||||||
|
|||||||
|
@@ -271,6 +271,10 @@ const (
|
|||||||
// Log schema validation errors so they can be analyzed later
|
// Log schema validation errors so they can be analyzed later
|
||||||
FlagDashboardSchemaValidationLogging = "dashboardSchemaValidationLogging"
|
FlagDashboardSchemaValidationLogging = "dashboardSchemaValidationLogging"
|
||||||
|
|
||||||
|
// FlagScanRowInvalidDashboardParseFallbackEnabled
|
||||||
|
// Enable fallback parsing behavior when scan row encounters invalid dashboard JSON
|
||||||
|
FlagScanRowInvalidDashboardParseFallbackEnabled = "scanRowInvalidDashboardParseFallbackEnabled"
|
||||||
|
|
||||||
// FlagDatasourceQueryTypes
|
// FlagDatasourceQueryTypes
|
||||||
// Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)
|
// Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)
|
||||||
FlagDatasourceQueryTypes = "datasourceQueryTypes"
|
FlagDatasourceQueryTypes = "datasourceQueryTypes"
|
||||||
|
|||||||
@@ -2760,6 +2760,18 @@
|
|||||||
"codeowner": "@grafana/identity-access-team"
|
"codeowner": "@grafana/identity-access-team"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "scanRowInvalidDashboardParseFallbackEnabled",
|
||||||
|
"resourceVersion": "1753730899886",
|
||||||
|
"creationTimestamp": "2025-07-28T19:28:19Z"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"description": "Enable fallback parsing behavior when scan row encounters invalid dashboard JSON",
|
||||||
|
"stage": "experimental",
|
||||||
|
"codeowner": "@grafana/search-and-storage"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "scopeApi",
|
"name": "scopeApi",
|
||||||
|
|||||||
Reference in New Issue
Block a user