feat: provides MT Dashboard service (#110447)

This commit is contained in:
Costa Alexoglou
2025-09-03 20:41:37 +00:00
committed by GitHub
parent 7d32640179
commit 3d2cef5f07
43 changed files with 284 additions and 69 deletions
+22 -1
View File
@@ -1,6 +1,10 @@
package conversion
import (
"context"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/utils/ptr"
@@ -10,6 +14,7 @@ import (
dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
"github.com/grafana/grafana/apps/dashboard/pkg/migration"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
"k8s.io/apiserver/pkg/endpoints/request"
)
func Convert_V0_to_V1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error {
@@ -23,7 +28,23 @@ func Convert_V0_to_V1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope convers
},
}
if err := migration.Migrate(out.Spec.Object, schemaversion.LATEST_VERSION); err != nil {
// the scope passed into this function is used in k8s apimachinery for migrations, but we also need the context
// to have what grafana expects in the request context, so that we can retrieve datasources for migrating
// some of the old dashboard schemas (these migrations used to be run in the frontend)
ctx := request.WithNamespace(context.Background(), in.GetNamespace())
nsInfo, err := types.ParseNamespace(in.GetNamespace())
if err != nil {
out.Status.Conversion.Failed = true
out.Status.Conversion.Error = ptr.To(err.Error())
return nil
}
// a background service identity is used here because the user who is reading the specific dashboard
// may not have access to all the datasources in the dashboard, but the migration still needs to take place
// in order to be able to convert between k8s versions (so that we have a guaranteed structure to convert between)
ctx, _ = identity.WithServiceIdentity(ctx, nsInfo.OrgID)
if err := migration.Migrate(ctx, out.Spec.Object, schemaversion.LATEST_VERSION); err != nil {
out.Status.Conversion.Failed = true
out.Status.Conversion.Error = ptr.To(err.Error())
return nil
+5 -4
View File
@@ -1,6 +1,7 @@
package migration
import (
"context"
"fmt"
"sync"
@@ -14,8 +15,8 @@ func Initialize(dsInfoProvider schemaversion.DataSourceInfoProvider, panelProvid
// Migrate migrates the given dashboard to the target version.
// This will block until the migrator is initialized.
func Migrate(dash map[string]interface{}, targetVersion int) error {
return migratorInstance.migrate(dash, targetVersion)
func Migrate(ctx context.Context, dash map[string]interface{}, targetVersion int) error {
return migratorInstance.migrate(ctx, dash, targetVersion)
}
var (
@@ -38,7 +39,7 @@ func (m *migrator) init(dsInfoProvider schemaversion.DataSourceInfoProvider, pan
})
}
func (m *migrator) migrate(dash map[string]interface{}, targetVersion int) error {
func (m *migrator) migrate(ctx context.Context, dash map[string]interface{}, targetVersion int) error {
if dash == nil {
return schemaversion.NewMigrationError("dashboard is nil", 0, targetVersion, "")
}
@@ -57,7 +58,7 @@ func (m *migrator) migrate(dash map[string]interface{}, targetVersion int) error
for nextVersion := inputVersion + 1; nextVersion <= targetVersion; nextVersion++ {
if migration, ok := m.migrations[nextVersion]; ok {
if err := migration(dash); err != nil {
if err := migration(ctx, dash); err != nil {
functionName := fmt.Sprintf("V%d", nextVersion)
return schemaversion.NewMigrationError("migration failed: "+err.Error(), inputVersion, nextVersion, functionName)
}
+8 -7
View File
@@ -2,6 +2,7 @@ package migration_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
@@ -29,7 +30,7 @@ func TestMigrate(t *testing.T) {
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
t.Run("minimum version check", func(t *testing.T) {
err := migration.Migrate(map[string]interface{}{
err := migration.Migrate(context.Background(), map[string]interface{}{
"schemaVersion": schemaversion.MIN_VERSION - 1,
}, schemaversion.MIN_VERSION)
@@ -52,7 +53,7 @@ func TestMigrate(t *testing.T) {
t.Run("input check "+f.Name(), func(t *testing.T) {
// use input version as the target version to ensure there are no changes
require.NoError(t, migration.Migrate(inputDash, inputVersion), "input check migration failed")
require.NoError(t, migration.Migrate(context.Background(), inputDash, inputVersion), "input check migration failed")
outBytes, err := json.MarshalIndent(inputDash, "", " ")
require.NoError(t, err, "failed to marshal migrated dashboard")
// We can ignore gosec G304 here since it's a test
@@ -71,7 +72,7 @@ func TestMigrate(t *testing.T) {
func testMigration(t *testing.T, dash map[string]interface{}, inputFileName string, targetVersion int) {
t.Helper()
require.NoError(t, migration.Migrate(dash, targetVersion), "%d migration failed", targetVersion)
require.NoError(t, migration.Migrate(context.Background(), dash, targetVersion), "%d migration failed", targetVersion)
outPath := filepath.Join(OUTPUT_DIR, inputFileName)
outBytes, err := json.MarshalIndent(dash, "", " ")
@@ -191,7 +192,7 @@ func TestSchemaMigrationMetrics(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Execute migration
err := migration.Migrate(tt.dashboard, tt.targetVersion)
err := migration.Migrate(context.Background(), tt.dashboard, tt.targetVersion)
// Check error expectation
if tt.expectSuccess {
@@ -261,7 +262,7 @@ func TestSchemaMigrationLogging(t *testing.T) {
// and check that the migration behaves correctly (logs are called internally)
// Execute migration
err := migration.Migrate(tt.dashboard, tt.targetVersion)
err := migration.Migrate(context.Background(), tt.dashboard, tt.targetVersion)
// Check error expectation
if tt.expectSuccess {
@@ -295,7 +296,7 @@ func TestLogMessageStructure(t *testing.T) {
}
// Successful migration - should trigger debug log
err := migration.Migrate(dashboard, schemaversion.LATEST_VERSION)
err := migration.Migrate(context.Background(), dashboard, schemaversion.LATEST_VERSION)
require.NoError(t, err, "migration should succeed")
// Failed migration - should trigger error log
@@ -303,7 +304,7 @@ func TestLogMessageStructure(t *testing.T) {
"schemaVersion": schemaversion.MIN_VERSION - 1,
"title": "old dashboard",
}
err = migration.Migrate(oldDashboard, schemaversion.LATEST_VERSION)
err = migration.Migrate(context.Background(), oldDashboard, schemaversion.LATEST_VERSION)
require.Error(t, err, "migration should fail")
// Both cases above execute the logging code in reportMigrationMetrics
@@ -2,6 +2,8 @@ package schemaversion
import (
"strconv"
"golang.org/x/net/context"
)
const (
@@ -9,7 +11,7 @@ const (
LATEST_VERSION = 41
)
type SchemaVersionMigrationFunc func(map[string]interface{}) error
type SchemaVersionMigrationFunc func(context.Context, map[string]interface{}) error
type DataSourceInfo struct {
Default bool
@@ -21,7 +23,9 @@ type DataSourceInfo struct {
}
type DataSourceInfoProvider interface {
GetDataSourceInfo() []DataSourceInfo
// GetDataSourceInfo returns a list of all data sources with their info
// The context must have the namespace in it
GetDataSourceInfo(ctx context.Context) []DataSourceInfo
}
type PanelPluginInfo struct {
@@ -1,6 +1,7 @@
package schemaversion_test
import (
"context"
"testing"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
@@ -68,7 +69,7 @@ func runMigrationTests(t *testing.T, testCases []migrationTestCase, migrationFun
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
err := migrationFunc(tt.input)
err := migrationFunc(context.Background(), tt.input)
if tt.expectedError != "" {
require.Error(t, err)
require.Equal(t, tt.expectedError, err.Error())
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V14 migrates the sharedCrosshair boolean property to graphTooltip integer property.
// This migration converts the old boolean shared crosshair setting to the new integer-based
// graph tooltip setting for consistency with updated dashboard tooltip behavior.
@@ -20,7 +22,7 @@ package schemaversion
// "panels": [...]
// }
func V14(dashboard map[string]interface{}) error {
func V14(_ context.Context, dashboard map[string]interface{}) error {
// Convert sharedCrosshair boolean to graphTooltip integer
sharedCrosshair := GetBoolValue(dashboard, "sharedCrosshair")
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V15 migration is a no-op migration
// It only updates the schema version to 15
// It's created to keep the migration history consistent with frontend migrator
@@ -19,7 +21,7 @@ package schemaversion
// "panels": [...]
// }
func V15(dashboard map[string]interface{}) error {
func V15(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 15
return nil
}
@@ -1,6 +1,7 @@
package schemaversion
import (
"context"
"math"
)
@@ -16,7 +17,7 @@ const (
// V16 migrates dashboard layout from the old row-based system to the modern grid-based layout.
// This migration follows the exact logic from DashboardMigrator.ts to ensure consistency between frontend and backend.
func V16(dashboard map[string]interface{}) error {
func V16(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 16
upgradeToGridLayout(dashboard)
@@ -1,6 +1,7 @@
package schemaversion
import (
"context"
"math"
"sort"
)
@@ -36,7 +37,7 @@ import (
// ]
//
// The minSpan property is removed after conversion.
func V17(dashboard map[string]interface{}) error {
func V17(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 17
panels, ok := dashboard["panels"].([]interface{})
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V18 migrates gauge panel options from the legacy `options-gauge` format to the new `options` format.
// This migration restructures gauge panel configuration to use the modern options structure with valueOptions.
//
@@ -42,7 +44,7 @@ package schemaversion
// }
// }
// ]
func V18(dashboard map[string]interface{}) error {
func V18(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 18
panels, ok := dashboard["panels"].([]interface{})
@@ -1,6 +1,7 @@
package schemaversion
import (
"context"
"regexp"
"strings"
)
@@ -36,7 +37,7 @@ import (
// ]
// }
// ]
func V19(dashboard map[string]interface{}) error {
func V19(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 19
panels, ok := dashboard["panels"].([]interface{})
@@ -1,6 +1,7 @@
package schemaversion
import (
"context"
"regexp"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/utils"
@@ -64,7 +65,7 @@ import (
// }
// }
// ]
func V20(dashboard map[string]interface{}) error {
func V20(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 20
panels, ok := dashboard["panels"].([]interface{})
@@ -1,6 +1,7 @@
package schemaversion
import (
"context"
"strings"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/utils"
@@ -55,7 +56,7 @@ import (
// }
// }
// ]
func V21(dashboard map[string]interface{}) error {
func V21(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 21
panels, ok := dashboard["panels"].([]interface{})
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V22 migrates table panel styles to set align property to 'auto'.
// This migration ensures that all table panel styles have their align property
// set to 'auto' for consistent alignment behavior.
@@ -27,7 +29,7 @@ package schemaversion
// ]
// }
// ]
func V22(dashboard map[string]interface{}) error {
func V22(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 22
panels, ok := dashboard["panels"].([]interface{})
@@ -1,6 +1,10 @@
package schemaversion
import "github.com/grafana/grafana/apps/dashboard/pkg/migration/utils"
import (
"context"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/utils"
)
// V23 migrates multi variables to ensure their current property is aligned with their multi property.
// This migration ensures that variables with multi=true have current.value and current.text as arrays,
@@ -23,7 +27,7 @@ import "github.com/grafana/grafana/apps/dashboard/pkg/migration/utils"
// { "type": "query", "multi": false, "current": { "value": "B", "text": "B" } }
// ]
// }
func V23(dashboard map[string]interface{}) error {
func V23(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 23
templating, ok := dashboard["templating"].(map[string]interface{})
@@ -1,6 +1,7 @@
package schemaversion
import (
"context"
"strconv"
)
@@ -199,7 +200,7 @@ func V24(panelProvider PanelPluginInfoProvider) SchemaVersionMigrationFunc {
return migrator.migrate
}
func (m *v24Migrator) migrate(dashboard map[string]interface{}) error {
func (m *v24Migrator) migrate(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 24
panels, ok := dashboard["panels"].([]interface{})
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V25 migration is a no-op migration
// It only updates the schema version to 25
// It's created to keep the migration history consistent with frontend migrator
@@ -43,7 +45,7 @@ package schemaversion
// }
// }
func V25(dashboard map[string]interface{}) error {
func V25(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = int(25)
return nil
}
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V26 migration performs two main tasks:
// 1. Converts all text2 panels to text panels by changing the type field
// 2. Removes the angular field from panel options if it exists
@@ -80,7 +82,7 @@ package schemaversion
// "title": "Graph Panel"
// }
// ]
func V26(dashboard map[string]interface{}) error {
func V26(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 26
panels, ok := dashboard["panels"].([]interface{})
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V27 migrates repeated panels and constant variables.
//
// The migration performs two main tasks:
@@ -73,7 +75,7 @@ package schemaversion
// }
// ]
// }
func V27(dashboard map[string]interface{}) error {
func V27(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 27
// Remove repeated panels
@@ -1,6 +1,7 @@
package schemaversion
import (
"context"
"fmt"
"strconv"
"strings"
@@ -66,12 +67,12 @@ func V28(panelProvider PanelPluginInfoProvider) SchemaVersionMigrationFunc {
statPanelVersion: statPanelVersion,
}
return func(dashboard map[string]interface{}) error {
return migrator.migrate(dashboard)
return func(ctx context.Context, dashboard map[string]interface{}) error {
return migrator.migrate(context.Background(), dashboard)
}
}
func (m *v28Migrator) migrate(dashboard map[string]interface{}) error {
func (m *v28Migrator) migrate(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 28
// Migrate singlestat panels
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V29 migrates query variables to ensure their refresh property is set to 1 (on dashboard load)
// if it is not 1 or 2, and clears their options array if present.
//
@@ -22,7 +24,7 @@ package schemaversion
// { "type": "query", "refresh": 1, "options": [] }
// ]
// }
func V29(dashboard map[string]interface{}) error {
func V29(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 29
templating, ok := dashboard["templating"].(map[string]interface{})
@@ -1,6 +1,7 @@
package schemaversion
import (
"context"
"strconv"
)
@@ -89,7 +90,7 @@ import (
// "tooltip": { "mode": "multi" }
// }
// }
func V30(dashboard map[string]interface{}) error {
func V30(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 30
panels, ok := dashboard["panels"].([]interface{})
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V31 adds a merge transformer after any labelsToFields transformer in panel transformations.
//
// This migration addresses data processing workflow optimization by automatically inserting
@@ -48,7 +50,7 @@ package schemaversion
// { "id": "merge", "options": {} }
// ]
// }
func V31(dashboard map[string]interface{}) error {
func V31(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = int(31)
panels, ok := dashboard["panels"].([]interface{})
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V32 is a no-op migration that serves as a placeholder for consistency.
//
// The migration performs no modifications to the dashboard structure and simply
@@ -21,7 +23,7 @@ package schemaversion
// "schemaVersion": 32,
// "panels": [...] // unchanged
// }
func V32(dashboard map[string]interface{}) error {
func V32(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = int(32)
return nil
}
@@ -1,5 +1,9 @@
package schemaversion
import (
"context"
)
// V33 migrates panel datasource references from string names to UIDs.
//
// This migration addresses datasource references in dashboard panels and their targets
@@ -57,8 +61,8 @@ package schemaversion
// ]
// }
func V33(dsInfo DataSourceInfoProvider) SchemaVersionMigrationFunc {
datasources := dsInfo.GetDataSourceInfo()
return func(dashboard map[string]interface{}) error {
return func(ctx context.Context, dashboard map[string]interface{}) error {
datasources := dsInfo.GetDataSourceInfo(ctx)
if dashboard == nil {
dashboard = map[string]interface{}{}
}
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V34 migrates CloudWatch queries that use multiple statistics into separate queries.
//
// This migration addresses CloudWatch queries where a single query uses multiple statistics
@@ -53,7 +55,7 @@ package schemaversion
// { name: "CloudWatch Alerts - Maximum", dimensions: {"InstanceId": "i-123"}, namespace: "AWS/EC2", region: "us-east-1", prefixMatching: false, statistic: "Maximum" },
// { name: "CloudWatch Alerts - Minimum", dimensions: {"InstanceId": "i-123"}, namespace: "AWS/EC2", region: "us-east-1", prefixMatching: false, statistic: "Minimum" }
// ]
func V34(dashboard map[string]interface{}) error {
func V34(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = int(34)
// Migrate panel queries if panels exist
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V35 ensures x-axis visibility in timeseries panels to prevent dashboard breakage.
//
// This migration addresses a specific issue where timeseries panels with all axes
@@ -33,7 +35,7 @@ package schemaversion
// properties: [{ id: "custom.axisPlacement", value: "auto" }]
// }]
// }
func V35(dashboard map[string]interface{}) error {
func V35(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = int(35)
panels, ok := dashboard["panels"].([]interface{})
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V36 migrates dashboard datasource references from legacy string format to structured UID-based objects.
//
// This migration addresses a critical evolution in Grafana's datasource architecture where datasource
@@ -73,8 +75,8 @@ package schemaversion
// }]
// }
func V36(dsInfo DataSourceInfoProvider) SchemaVersionMigrationFunc {
datasources := dsInfo.GetDataSourceInfo()
return func(dashboard map[string]interface{}) error {
return func(ctx context.Context, dashboard map[string]interface{}) error {
datasources := dsInfo.GetDataSourceInfo(ctx)
dashboard["schemaVersion"] = int(36)
migrateAnnotations(dashboard, datasources)
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V37 normalizes legend configuration to use `showLegend` property consistently.
//
// This migration addresses inconsistencies in how legend visibility was handled.
@@ -71,7 +73,7 @@ package schemaversion
// showLegend: true
// }
// }
func V37(dashboard map[string]interface{}) error {
func V37(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = int(37)
panels, ok := dashboard["panels"].([]interface{})
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V38 migrates table panel configuration from displayMode to the structured cellOptions format.
//
// This migration addresses limitations in the original table panel cell display configuration where
@@ -70,7 +72,7 @@ package schemaversion
// }
// }]
// }]
func V38(dashboard map[string]interface{}) error {
func V38(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = int(38)
panels, ok := dashboard["panels"].([]interface{})
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V39 migrates timeSeriesTable transformation configuration to support extensible per-query options.
//
// This migration addresses limitations in the original timeSeriesTable transformation design where
@@ -45,7 +47,7 @@ package schemaversion
// "C": { stat: "last" }
// }
// }]
func V39(dashboard map[string]interface{}) error {
func V39(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = int(39)
panels, ok := dashboard["panels"].([]interface{})
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V40 normalizes the dashboard refresh property to ensure consistent string typing.
//
// This migration addresses type inconsistencies in dashboard refresh configuration that could
@@ -33,7 +35,7 @@ package schemaversion
// refresh: "" // normalized to empty string
// refresh: "" // normalized to empty string
// refresh: "" // property added with empty string
func V40(dash map[string]interface{}) error {
func V40(_ context.Context, dash map[string]interface{}) error {
dash["schemaVersion"] = int(40)
if _, ok := dash["refresh"].(string); !ok {
dash["refresh"] = ""
@@ -1,5 +1,7 @@
package schemaversion
import "context"
// V41 removes the deprecated time_options property from dashboard timepicker configuration.
//
// This migration addresses technical debt by cleaning up legacy timepicker settings that have
@@ -30,7 +32,7 @@ package schemaversion
// timepicker: {
// refresh_intervals: ["5s", "10s", "30s", "1m"]
// }
func V41(dash map[string]interface{}) error {
func V41(_ context.Context, dash map[string]interface{}) error {
dash["schemaVersion"] = int(41)
if timepicker, ok := dash["timepicker"].(map[string]interface{}); ok {
// time_options is a legacy property that was not used since grafana version 5
@@ -1,6 +1,8 @@
package testutil
import (
"context"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)
@@ -10,7 +12,7 @@ type TestPanelProvider struct {
customPanels []schemaversion.PanelPluginInfo
}
func (m *TestDataSourceProvider) GetDataSourceInfo() []schemaversion.DataSourceInfo {
func (m *TestDataSourceProvider) GetDataSourceInfo(_ context.Context) []schemaversion.DataSourceInfo {
return []schemaversion.DataSourceInfo{
{
Default: true,