Dashboard Migrations: V8 - update old influxdb schema (#111423)
* migrate to v19 * migrate to v18 * Migration to be verified: v17 Convert minSpan to maxPerRow in panels * Migration to be verified: 16 Grid layout migration * Refactor v17 and v19 migrations to use shared helper functions * Migration to be verified: 15 No-op migration for schema consistency * Migration to be verified: 14 Shared crosshair to graph tooltip migration * cleanup * wip * complete migration * fix lint issues * refactor and test with minimal graph config * update tests * migrate to v12 * extract defaults outside the func * lint * lint * add missing showValues prop * migrate to v11 * migrate to v10 * add test files * update * migrate to v9 * migrate to v8 * add context and fix latest version * add context * add context * generate snapshots * v13 should be no-op * clean up * fix tests * add context * snapshots * generate snapshots * update * snapshots * fix test * remove v28 * remove singlestat migraiton from frontend migrator because this is an automigration * remove unused function * Remove v24 table plugin logic * cleanup * remove plugin version for automigrate as it was used only in v24 and v28 that have been removed * cleanup * update snapshot * update snapshot * update snapshot --------- Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
MIN_VERSION = 8
|
||||
MIN_VERSION = 7
|
||||
LATEST_VERSION = 42
|
||||
)
|
||||
|
||||
@@ -35,6 +35,7 @@ type PanelPluginInfo struct {
|
||||
|
||||
func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc {
|
||||
return map[int]SchemaVersionMigrationFunc{
|
||||
8: V8,
|
||||
9: V9,
|
||||
10: V10,
|
||||
11: V11,
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package schemaversion
|
||||
|
||||
import "context"
|
||||
|
||||
// V8 migration updates old InfluxDB query schema to the new format.
|
||||
// This migration transforms the legacy InfluxDB query structure with fields, tags, and groupBy
|
||||
// into the newer select-based query format.
|
||||
//
|
||||
// Background:
|
||||
// In earlier versions, InfluxDB queries were stored using a different schema with separate
|
||||
// fields, tags, and groupBy properties. This migration converts them to the newer select
|
||||
// format that is more structured and easier to work with.
|
||||
//
|
||||
// The migration handles two cases:
|
||||
// 1. Raw queries: Simply removes the fields and fill properties
|
||||
// 2. Structured queries: Converts fields to select format and updates groupBy structure
|
||||
//
|
||||
// Example before migration (structured query):
|
||||
// {
|
||||
// "schemaVersion": 7,
|
||||
// "panels": [
|
||||
// {
|
||||
// "targets": [
|
||||
// {
|
||||
// "fields": [
|
||||
// {"name": "value", "func": "mean", "mathExpr": "*2", "asExpr": "doubled"}
|
||||
// ],
|
||||
// "tags": [{"key": "host", "value": "server1"}],
|
||||
// "groupBy": [
|
||||
// {"type": "time", "interval": "1m"},
|
||||
// {"type": "tag", "key": "host"}
|
||||
// ],
|
||||
// "fill": "null"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// Example after migration (structured query):
|
||||
// {
|
||||
// "schemaVersion": 8,
|
||||
// "panels": [
|
||||
// {
|
||||
// "targets": [
|
||||
// {
|
||||
// "select": [
|
||||
// [
|
||||
// {"type": "field", "params": ["value"]},
|
||||
// {"type": "mean", "params": []},
|
||||
// {"type": "math", "params": ["*2"]},
|
||||
// {"type": "alias", "params": ["doubled"]}
|
||||
// ]
|
||||
// ],
|
||||
// "tags": [{"key": "host", "value": "server1"}],
|
||||
// "groupBy": [
|
||||
// {"type": "time", "params": ["1m"]},
|
||||
// {"type": "tag", "params": ["host"]},
|
||||
// {"type": "fill", "params": ["null"]}
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
func V8(_ context.Context, dashboard map[string]interface{}) error {
|
||||
dashboard["schemaVersion"] = 8
|
||||
|
||||
panels, ok := dashboard["panels"].([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, p := range panels {
|
||||
panel, ok := p.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
targets, ok := panel["targets"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, t := range targets {
|
||||
target, ok := t.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this target has the old InfluxDB schema
|
||||
fields, hasFields := target["fields"]
|
||||
_, hasTags := target["tags"]
|
||||
groupBy, hasGroupBy := target["groupBy"]
|
||||
|
||||
if !hasFields || !hasTags || !hasGroupBy {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this is a raw query
|
||||
rawQuery, isRawQuery := target["rawQuery"].(bool)
|
||||
if isRawQuery && rawQuery {
|
||||
// For raw queries, just delete fields and fill
|
||||
delete(target, "fields")
|
||||
delete(target, "fill")
|
||||
} else {
|
||||
// For structured queries, convert fields to select format
|
||||
fieldsArray, ok := fields.([]interface{})
|
||||
if ok {
|
||||
selectArray := make([]interface{}, 0, len(fieldsArray))
|
||||
|
||||
for _, f := range fieldsArray {
|
||||
field, ok := f.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := make([]interface{}, 0)
|
||||
|
||||
// Add field part
|
||||
if name, ok := field["name"].(string); ok {
|
||||
parts = append(parts, map[string]interface{}{
|
||||
"type": "field",
|
||||
"params": []interface{}{name},
|
||||
})
|
||||
}
|
||||
|
||||
// Add function part
|
||||
if funcName, ok := field["func"].(string); ok {
|
||||
parts = append(parts, map[string]interface{}{
|
||||
"type": funcName,
|
||||
"params": []interface{}{},
|
||||
})
|
||||
}
|
||||
|
||||
// Add math expression if present
|
||||
if mathExpr, ok := field["mathExpr"].(string); ok {
|
||||
parts = append(parts, map[string]interface{}{
|
||||
"type": "math",
|
||||
"params": []interface{}{mathExpr},
|
||||
})
|
||||
}
|
||||
|
||||
// Add alias if present
|
||||
if asExpr, ok := field["asExpr"].(string); ok {
|
||||
parts = append(parts, map[string]interface{}{
|
||||
"type": "alias",
|
||||
"params": []interface{}{asExpr},
|
||||
})
|
||||
}
|
||||
|
||||
if len(parts) > 0 {
|
||||
selectArray = append(selectArray, parts)
|
||||
}
|
||||
}
|
||||
|
||||
target["select"] = selectArray
|
||||
}
|
||||
|
||||
// Remove the old fields property
|
||||
delete(target, "fields")
|
||||
|
||||
// Update groupBy format
|
||||
if groupByArray, ok := groupBy.([]interface{}); ok {
|
||||
for _, gb := range groupByArray {
|
||||
groupByPart, ok := gb.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert time groupBy
|
||||
if partType, ok := groupByPart["type"].(string); ok && partType == "time" {
|
||||
if interval, ok := groupByPart["interval"].(string); ok {
|
||||
groupByPart["params"] = []interface{}{interval}
|
||||
delete(groupByPart, "interval")
|
||||
}
|
||||
}
|
||||
|
||||
// Convert tag groupBy
|
||||
if partType, ok := groupByPart["type"].(string); ok && partType == "tag" {
|
||||
if key, ok := groupByPart["key"].(string); ok {
|
||||
groupByPart["params"] = []interface{}{key}
|
||||
delete(groupByPart, "key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add fill to groupBy if present
|
||||
if fill, hasFill := target["fill"]; hasFill {
|
||||
newGroupByArray := make([]interface{}, len(groupByArray))
|
||||
copy(newGroupByArray, groupByArray)
|
||||
newGroupByArray = append(newGroupByArray, map[string]interface{}{
|
||||
"type": "fill",
|
||||
"params": []interface{}{fill},
|
||||
})
|
||||
target["groupBy"] = newGroupByArray
|
||||
delete(target, "fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package schemaversion_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
)
|
||||
|
||||
func TestV8(t *testing.T) {
|
||||
tests := []migrationTestCase{
|
||||
{
|
||||
name: "InfluxDB structured query should be converted to new select format",
|
||||
input: map[string]interface{}{
|
||||
"title": "V8 InfluxDB Query Migration Test Dashboard",
|
||||
"schemaVersion": 7,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{
|
||||
"fields": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "value",
|
||||
"func": "mean",
|
||||
"mathExpr": "*2",
|
||||
"asExpr": "doubled",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "count",
|
||||
"func": "sum",
|
||||
},
|
||||
},
|
||||
"tags": []interface{}{
|
||||
map[string]interface{}{
|
||||
"key": "host",
|
||||
"value": "server1",
|
||||
},
|
||||
},
|
||||
"groupBy": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "time",
|
||||
"interval": "1m",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "tag",
|
||||
"key": "host",
|
||||
},
|
||||
},
|
||||
"fill": "null",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V8 InfluxDB Query Migration Test Dashboard",
|
||||
"schemaVersion": 8,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{
|
||||
"select": []interface{}{
|
||||
[]interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "field",
|
||||
"params": []interface{}{"value"},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "mean",
|
||||
"params": []interface{}{},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "math",
|
||||
"params": []interface{}{"*2"},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "alias",
|
||||
"params": []interface{}{"doubled"},
|
||||
},
|
||||
},
|
||||
[]interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "field",
|
||||
"params": []interface{}{"count"},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "sum",
|
||||
"params": []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
"tags": []interface{}{
|
||||
map[string]interface{}{
|
||||
"key": "host",
|
||||
"value": "server1",
|
||||
},
|
||||
},
|
||||
"groupBy": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "time",
|
||||
"params": []interface{}{"1m"},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "tag",
|
||||
"params": []interface{}{"host"},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "fill",
|
||||
"params": []interface{}{"null"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "InfluxDB raw query should only remove fields and fill",
|
||||
input: map[string]interface{}{
|
||||
"title": "V8 InfluxDB Raw Query Migration Test Dashboard",
|
||||
"schemaVersion": 7,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{
|
||||
"rawQuery": true,
|
||||
"fields": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "value",
|
||||
"func": "mean",
|
||||
},
|
||||
},
|
||||
"tags": []interface{}{
|
||||
map[string]interface{}{
|
||||
"key": "host",
|
||||
"value": "server1",
|
||||
},
|
||||
},
|
||||
"groupBy": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "time",
|
||||
"interval": "1m",
|
||||
},
|
||||
},
|
||||
"fill": "null",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V8 InfluxDB Raw Query Migration Test Dashboard",
|
||||
"schemaVersion": 8,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{
|
||||
"rawQuery": true,
|
||||
"tags": []interface{}{
|
||||
map[string]interface{}{
|
||||
"key": "host",
|
||||
"value": "server1",
|
||||
},
|
||||
},
|
||||
"groupBy": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "time",
|
||||
"interval": "1m",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "targets without old InfluxDB schema should remain unchanged",
|
||||
input: map[string]interface{}{
|
||||
"title": "V8 Non-InfluxDB Target Test Dashboard",
|
||||
"schemaVersion": 7,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{
|
||||
"expr": "up",
|
||||
"refId": "A",
|
||||
"format": "time_series",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V8 Non-InfluxDB Target Test Dashboard",
|
||||
"schemaVersion": 8,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{
|
||||
"expr": "up",
|
||||
"refId": "A",
|
||||
"format": "time_series",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "panels without targets should remain unchanged",
|
||||
input: map[string]interface{}{
|
||||
"title": "V8 No Targets Test Dashboard",
|
||||
"schemaVersion": 7,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V8 No Targets Test Dashboard",
|
||||
"schemaVersion": 8,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard without panels should only update schema version",
|
||||
input: map[string]interface{}{
|
||||
"title": "V8 No Panels Test Dashboard",
|
||||
"schemaVersion": 7,
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V8 No Panels Test Dashboard",
|
||||
"schemaVersion": 8,
|
||||
},
|
||||
},
|
||||
}
|
||||
runMigrationTests(t, tests, schemaversion.V8)
|
||||
}
|
||||
Reference in New Issue
Block a user