Schema v2: Move annotation mappings out of legacyOptions (#114721)
* Add mappings property to v2(alpha/beta)1 * Mappings convetions * OpenAPI spec * fe annotaations mapping * Update snapshots * v2->v1 conversion * fix tests --------- Co-authored-by: Haris Rozajac <haris.rozajac12@gmail.com>
This commit is contained in:
co-authored by
Haris Rozajac
parent
3e50bfced8
commit
bb787a2637
+17
-5
@@ -154,10 +154,22 @@
|
||||
"exclude": true
|
||||
},
|
||||
"mappings": {
|
||||
"title": "service",
|
||||
"text": "description",
|
||||
"time": "timestamp",
|
||||
"tags": "labels"
|
||||
"title": {
|
||||
"source": "field",
|
||||
"value": "service"
|
||||
},
|
||||
"text": {
|
||||
"source": "field",
|
||||
"value": "description"
|
||||
},
|
||||
"time": {
|
||||
"source": "field",
|
||||
"value": "timestamp"
|
||||
},
|
||||
"tags": {
|
||||
"source": "field",
|
||||
"value": "labels"
|
||||
}
|
||||
},
|
||||
"builtIn": 0,
|
||||
"type": "influxdb"
|
||||
@@ -214,4 +226,4 @@
|
||||
},
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v0alpha1.json
Vendored
+17
-5
@@ -151,10 +151,22 @@
|
||||
"hide": false,
|
||||
"iconColor": "#FF5722",
|
||||
"mappings": {
|
||||
"tags": "labels",
|
||||
"text": "description",
|
||||
"time": "timestamp",
|
||||
"title": "service"
|
||||
"tags": {
|
||||
"source": "field",
|
||||
"value": "labels"
|
||||
},
|
||||
"text": {
|
||||
"source": "field",
|
||||
"value": "description"
|
||||
},
|
||||
"time": {
|
||||
"source": "field",
|
||||
"value": "timestamp"
|
||||
},
|
||||
"title": {
|
||||
"source": "field",
|
||||
"value": "service"
|
||||
}
|
||||
},
|
||||
"name": "Complex Filter Annotation",
|
||||
"target": {
|
||||
@@ -227,4 +239,4 @@
|
||||
"storedVersion": "v1beta1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json
Vendored
+18
-6
@@ -215,13 +215,25 @@
|
||||
3
|
||||
]
|
||||
},
|
||||
"legacyOptions": {
|
||||
"mappings": {
|
||||
"tags": "labels",
|
||||
"text": "description",
|
||||
"time": "timestamp",
|
||||
"title": "service"
|
||||
"mappings": {
|
||||
"tags": {
|
||||
"source": "field",
|
||||
"value": "labels"
|
||||
},
|
||||
"text": {
|
||||
"source": "field",
|
||||
"value": "description"
|
||||
},
|
||||
"time": {
|
||||
"source": "field",
|
||||
"value": "timestamp"
|
||||
},
|
||||
"title": {
|
||||
"source": "field",
|
||||
"value": "service"
|
||||
}
|
||||
},
|
||||
"legacyOptions": {
|
||||
"type": "influxdb"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+18
-6
@@ -224,13 +224,25 @@
|
||||
3
|
||||
]
|
||||
},
|
||||
"legacyOptions": {
|
||||
"mappings": {
|
||||
"tags": "labels",
|
||||
"text": "description",
|
||||
"time": "timestamp",
|
||||
"title": "service"
|
||||
"mappings": {
|
||||
"tags": {
|
||||
"source": "field",
|
||||
"value": "labels"
|
||||
},
|
||||
"text": {
|
||||
"source": "field",
|
||||
"value": "description"
|
||||
},
|
||||
"time": {
|
||||
"source": "field",
|
||||
"value": "timestamp"
|
||||
},
|
||||
"title": {
|
||||
"source": "field",
|
||||
"value": "service"
|
||||
}
|
||||
},
|
||||
"legacyOptions": {
|
||||
"type": "influxdb"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1784,6 +1784,12 @@ func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.Da
|
||||
filter = buildAnnotationFilter(filterMap)
|
||||
}
|
||||
|
||||
// Transform mappings
|
||||
var mappings map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping
|
||||
if mappingsMap, ok := annotationMap["mappings"].(map[string]interface{}); ok && mappingsMap != nil {
|
||||
mappings = convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
}
|
||||
|
||||
// Transform builtIn from float64 to bool
|
||||
var builtInPtr *bool
|
||||
if builtInVal, ok := annotationMap["builtIn"]; ok && builtInVal != nil {
|
||||
@@ -1809,6 +1815,7 @@ func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.Da
|
||||
IconColor: schemaversion.GetStringValue(annotationMap, "iconColor", defaultAnnotationQuerySpec.IconColor),
|
||||
BuiltIn: builtInPtr,
|
||||
Filter: filter,
|
||||
Mappings: mappings,
|
||||
}
|
||||
|
||||
// Handle any additional properties in LegacyOptions
|
||||
@@ -1820,7 +1827,7 @@ func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.Da
|
||||
// Add other legacy fields if they exist
|
||||
for key, value := range annotationMap {
|
||||
switch key {
|
||||
case "name", "datasource", "enable", "hide", "iconColor", "filter", "target", "builtIn", "type":
|
||||
case "name", "datasource", "enable", "hide", "iconColor", "filter", "target", "builtIn", "type", "mappings":
|
||||
// Skip already handled fields
|
||||
default:
|
||||
legacyOptions[key] = value
|
||||
@@ -1866,6 +1873,52 @@ func buildAnnotationFilter(filterMap map[string]interface{}) *dashv2alpha1.Dashb
|
||||
return filter
|
||||
}
|
||||
|
||||
func convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap map[string]interface{}) map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping {
|
||||
mappings := make(map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping)
|
||||
|
||||
for key, value := range mappingsMap {
|
||||
mapping := dashv2alpha1.DashboardAnnotationEventFieldMapping{}
|
||||
|
||||
// Handle simple string format (v1beta1 legacy format: "fieldName": "targetFieldName")
|
||||
if valueStr, ok := value.(string); ok && valueStr != "" {
|
||||
// Simple string mapping: treat as field source with the value as the field name
|
||||
defaultSource := "field"
|
||||
mapping.Source = &defaultSource
|
||||
mapping.Value = &valueStr
|
||||
mappings[key] = mapping
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle object format (v2alpha1 format: "fieldName": {"source": "field", "value": "...", "regex": "..."})
|
||||
mappingMap, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract source (defaults to "field" if not specified)
|
||||
if source, ok := mappingMap["source"].(string); ok && source != "" {
|
||||
mapping.Source = &source
|
||||
} else {
|
||||
defaultSource := "field"
|
||||
mapping.Source = &defaultSource
|
||||
}
|
||||
|
||||
// Extract value (optional)
|
||||
if valueStr, ok := mappingMap["value"].(string); ok && valueStr != "" {
|
||||
mapping.Value = &valueStr
|
||||
}
|
||||
|
||||
// Extract regex (optional)
|
||||
if regex, ok := mappingMap["regex"].(string); ok && regex != "" {
|
||||
mapping.Regex = ®ex
|
||||
}
|
||||
|
||||
mappings[key] = mapping
|
||||
}
|
||||
|
||||
return mappings
|
||||
}
|
||||
|
||||
// Panel helper functions
|
||||
|
||||
func transformPanelQueries(ctx context.Context, panelMap map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) []dashv2alpha1.DashboardPanelQueryKind {
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
package conversion
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConvertAnnotationMappings_V1beta1_to_V2alpha1(t *testing.T) {
|
||||
t.Run("should convert mappings with all fields", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "service",
|
||||
"regex": "",
|
||||
},
|
||||
"text": map[string]interface{}{
|
||||
"source": "text",
|
||||
"value": "constant text",
|
||||
},
|
||||
"time": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "timestamp",
|
||||
},
|
||||
"tags": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "labels",
|
||||
"regex": "/(.*)/",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
require.Len(t, result, 4)
|
||||
|
||||
// Check title mapping
|
||||
titleMapping, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
assert.Nil(t, titleMapping.Regex)
|
||||
|
||||
// Check text mapping
|
||||
textMapping, ok := result["text"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", *textMapping.Source)
|
||||
assert.Equal(t, "constant text", *textMapping.Value)
|
||||
assert.Nil(t, textMapping.Regex)
|
||||
|
||||
// Check time mapping
|
||||
timeMapping, ok := result["time"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *timeMapping.Source)
|
||||
assert.Equal(t, "timestamp", *timeMapping.Value)
|
||||
assert.Nil(t, timeMapping.Regex)
|
||||
|
||||
// Check tags mapping
|
||||
tagsMapping, ok := result["tags"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *tagsMapping.Source)
|
||||
assert.Equal(t, "labels", *tagsMapping.Value)
|
||||
assert.Equal(t, "/(.*)/", *tagsMapping.Regex)
|
||||
})
|
||||
|
||||
t.Run("should default source to field when not specified", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"value": "service",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
titleMapping, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
})
|
||||
|
||||
t.Run("should handle empty source string by defaulting to field", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "",
|
||||
"value": "service",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
titleMapping, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
})
|
||||
|
||||
t.Run("should handle skip source", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "skip",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
titleMapping, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "skip", *titleMapping.Source)
|
||||
assert.Nil(t, titleMapping.Value)
|
||||
assert.Nil(t, titleMapping.Regex)
|
||||
})
|
||||
|
||||
t.Run("should skip invalid mapping entries", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "service",
|
||||
},
|
||||
"invalid": 123, // Invalid: not a string or map
|
||||
"text": map[string]interface{}{
|
||||
"source": "text",
|
||||
"value": "constant",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
// Should have 2 valid mappings (title and text)
|
||||
// String values are now treated as valid legacy format mappings
|
||||
require.Len(t, result, 2)
|
||||
_, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
_, ok = result["text"]
|
||||
require.True(t, ok)
|
||||
_, ok = result["invalid"]
|
||||
assert.False(t, ok, "invalid entry should be skipped")
|
||||
})
|
||||
|
||||
t.Run("should handle empty mappings map", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildAnnotationQuery_Mappings(t *testing.T) {
|
||||
t.Run("should extract mappings to top-level property", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
"mappings": map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "service",
|
||||
},
|
||||
"text": map[string]interface{}{
|
||||
"source": "text",
|
||||
"value": "constant text",
|
||||
},
|
||||
"time": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "timestamp",
|
||||
"regex": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify mappings are in the correct location
|
||||
require.NotNil(t, result.Spec.Mappings)
|
||||
assert.Len(t, result.Spec.Mappings, 3)
|
||||
|
||||
// Verify mappings content
|
||||
titleMapping, ok := result.Spec.Mappings["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
|
||||
textMapping, ok := result.Spec.Mappings["text"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", *textMapping.Source)
|
||||
assert.Equal(t, "constant text", *textMapping.Value)
|
||||
|
||||
// Verify mappings are NOT in legacyOptions
|
||||
if result.Spec.LegacyOptions != nil {
|
||||
_, hasMappingsInLegacy := result.Spec.LegacyOptions["mappings"]
|
||||
assert.False(t, hasMappingsInLegacy, "mappings should not be in legacyOptions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should handle annotation without mappings", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Mappings should be nil when not present
|
||||
assert.Nil(t, result.Spec.Mappings)
|
||||
})
|
||||
|
||||
t.Run("should handle empty mappings", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
"mappings": map[string]interface{}{},
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Empty mappings should result in empty map
|
||||
assert.NotNil(t, result.Spec.Mappings)
|
||||
assert.Empty(t, result.Spec.Mappings)
|
||||
})
|
||||
|
||||
t.Run("should exclude mappings from legacyOptions", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
"mappings": map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "service",
|
||||
},
|
||||
},
|
||||
"type": "prometheus",
|
||||
"customField": "customValue",
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify mappings are in the correct location
|
||||
require.NotNil(t, result.Spec.Mappings)
|
||||
assert.Len(t, result.Spec.Mappings, 1)
|
||||
|
||||
// Verify other fields are in legacyOptions
|
||||
require.NotNil(t, result.Spec.LegacyOptions)
|
||||
assert.Equal(t, "prometheus", result.Spec.LegacyOptions["type"])
|
||||
assert.Equal(t, "customValue", result.Spec.LegacyOptions["customField"])
|
||||
|
||||
// Verify mappings are NOT in legacyOptions
|
||||
_, hasMappingsInLegacy := result.Spec.LegacyOptions["mappings"]
|
||||
assert.False(t, hasMappingsInLegacy, "mappings should not be in legacyOptions")
|
||||
})
|
||||
|
||||
t.Run("should handle mappings with regex", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
"mappings": map[string]interface{}{
|
||||
"tags": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "labels",
|
||||
"regex": "/(.*)/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, result.Spec.Mappings)
|
||||
tagsMapping, ok := result.Spec.Mappings["tags"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *tagsMapping.Source)
|
||||
assert.Equal(t, "labels", *tagsMapping.Value)
|
||||
assert.Equal(t, "/(.*)/", *tagsMapping.Regex)
|
||||
})
|
||||
}
|
||||
@@ -1721,6 +1721,14 @@ func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryK
|
||||
}
|
||||
}
|
||||
|
||||
// Convert mappings from v2alpha1 format back to v1beta1 format
|
||||
if len(annotation.Spec.Mappings) > 0 {
|
||||
mappings := convertAnnotationMappings_V2alpha1_to_V1beta1(annotation.Spec.Mappings)
|
||||
if len(mappings) > 0 {
|
||||
annotationMap["mappings"] = mappings
|
||||
}
|
||||
}
|
||||
|
||||
// Copy legacy options
|
||||
// This is used to copy any unknown properties from the v1 at the root of the annotations that were not handled by the conversion.
|
||||
// When they are converted into V2 they are moved to legacyOptions. Now we move them back to the root of the annotation.
|
||||
@@ -1728,7 +1736,7 @@ func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryK
|
||||
for k, v := range annotation.Spec.LegacyOptions {
|
||||
// Skip fields already handled
|
||||
if k != "name" && k != "enable" && k != "hide" && k != "iconColor" &&
|
||||
k != "datasource" && k != "target" && k != "filter" && k != "builtIn" && k != "placement" {
|
||||
k != "datasource" && k != "target" && k != "filter" && k != "builtIn" && k != "placement" && k != "mappings" {
|
||||
annotationMap[k] = v
|
||||
}
|
||||
}
|
||||
@@ -1740,6 +1748,46 @@ func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryK
|
||||
return result
|
||||
}
|
||||
|
||||
// convertAnnotationMappings_V2alpha1_to_V1beta1 converts mappings from v2alpha1 structured format
|
||||
// back to v1beta1 format. v1beta1 supports both simple string format and structured format with source/value/regex.
|
||||
// v2alpha1 format: map[string]DashboardAnnotationEventFieldMapping with Source, Value, Regex
|
||||
// v1beta1 format: map[string]interface{} where values can be either:
|
||||
// - string (legacy simple format: "fieldName": "targetFieldName")
|
||||
// - object (structured format: "fieldName": {"source": "field", "value": "...", "regex": "..."})
|
||||
func convertAnnotationMappings_V2alpha1_to_V1beta1(mappings map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for key, mapping := range mappings {
|
||||
// Always convert to structured format with source and value fields
|
||||
mappingMap := make(map[string]interface{})
|
||||
|
||||
// Source defaults to "field" if not specified
|
||||
source := "field"
|
||||
if mapping.Source != nil {
|
||||
source = *mapping.Source
|
||||
}
|
||||
mappingMap["source"] = source
|
||||
|
||||
// Value is optional (required for "field" and "text" sources, but "skip" doesn't need it)
|
||||
if mapping.Value != nil && *mapping.Value != "" {
|
||||
mappingMap["value"] = *mapping.Value
|
||||
}
|
||||
|
||||
// Regex is optional
|
||||
if mapping.Regex != nil && *mapping.Regex != "" {
|
||||
mappingMap["regex"] = *mapping.Regex
|
||||
}
|
||||
|
||||
// Include the mapping if it has source (and value for non-skip sources)
|
||||
// Skip source doesn't require a value
|
||||
if source == "skip" || (mapping.Value != nil && *mapping.Value != "") {
|
||||
result[key] = mappingMap
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Enum transformation functions (reverse of v1→v2)
|
||||
func transformVariableHideFromEnum(hide dashv2alpha1.DashboardVariableHide) interface{} {
|
||||
switch hide {
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package conversion
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
)
|
||||
|
||||
func TestConvertAnnotationMappings_V2alpha1_to_V1beta1(t *testing.T) {
|
||||
t.Run("should convert simple field mappings to structured format with source and value", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("service"),
|
||||
},
|
||||
"text": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("description"),
|
||||
},
|
||||
"time": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("timestamp"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 3)
|
||||
// All mappings should be in structured format with source and value
|
||||
titleMapping, ok := result["title"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", titleMapping["source"])
|
||||
assert.Equal(t, "service", titleMapping["value"])
|
||||
|
||||
textMapping, ok := result["text"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", textMapping["source"])
|
||||
assert.Equal(t, "description", textMapping["value"])
|
||||
|
||||
timeMapping, ok := result["time"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", timeMapping["source"])
|
||||
assert.Equal(t, "timestamp", timeMapping["value"])
|
||||
})
|
||||
|
||||
t.Run("should convert mappings with default field source to structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: nil, // nil defaults to "field"
|
||||
Value: ptr.To("service"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
titleMapping, ok := result["title"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", titleMapping["source"])
|
||||
assert.Equal(t, "service", titleMapping["value"])
|
||||
})
|
||||
|
||||
t.Run("should preserve complex mappings with regex as structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"tags": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("labels"),
|
||||
Regex: ptr.To("/(.*)/"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
tagsMapping, ok := result["tags"].(map[string]interface{})
|
||||
require.True(t, ok, "tags mapping should be a map")
|
||||
assert.Equal(t, "field", tagsMapping["source"])
|
||||
assert.Equal(t, "labels", tagsMapping["value"])
|
||||
assert.Equal(t, "/(.*)/", tagsMapping["regex"])
|
||||
})
|
||||
|
||||
t.Run("should preserve mappings with non-field source as structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"text": {
|
||||
Source: ptr.To("text"),
|
||||
Value: ptr.To("constant text"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
textMapping, ok := result["text"].(map[string]interface{})
|
||||
require.True(t, ok, "text mapping should be a map")
|
||||
assert.Equal(t, "text", textMapping["source"])
|
||||
assert.Equal(t, "constant text", textMapping["value"])
|
||||
})
|
||||
|
||||
t.Run("should preserve mappings with skip source as structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: ptr.To("skip"),
|
||||
},
|
||||
"text": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("description"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 2)
|
||||
// Skip mapping should be preserved as structured format
|
||||
titleMapping, ok := result["title"].(map[string]interface{})
|
||||
require.True(t, ok, "skip mapping should be preserved as map")
|
||||
assert.Equal(t, "skip", titleMapping["source"])
|
||||
// Field mapping should be structured format with source and value
|
||||
textMapping, ok := result["text"].(map[string]interface{})
|
||||
require.True(t, ok, "field mapping should be structured format")
|
||||
assert.Equal(t, "field", textMapping["source"])
|
||||
assert.Equal(t, "description", textMapping["value"])
|
||||
})
|
||||
|
||||
t.Run("should skip mappings without value", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: ptr.To("field"),
|
||||
Value: nil,
|
||||
},
|
||||
"text": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To(""),
|
||||
},
|
||||
"time": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("timestamp"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
_, ok := result["title"]
|
||||
assert.False(t, ok, "mapping without value should be skipped")
|
||||
_, ok = result["text"]
|
||||
assert.False(t, ok, "mapping with empty value should be skipped")
|
||||
_, ok = result["time"]
|
||||
assert.True(t, ok, "mapping with value should be included")
|
||||
})
|
||||
|
||||
t.Run("should handle empty mappings map", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("should handle all mappings in structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("service"),
|
||||
},
|
||||
"tags": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("labels"),
|
||||
Regex: ptr.To("/(.*)/"),
|
||||
},
|
||||
"text": {
|
||||
Source: ptr.To("text"),
|
||||
Value: ptr.To("constant"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 3)
|
||||
// All mappings should be in structured format
|
||||
titleMapping, ok := result["title"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", titleMapping["source"])
|
||||
assert.Equal(t, "service", titleMapping["value"])
|
||||
|
||||
tagsMapping, ok := result["tags"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", tagsMapping["source"])
|
||||
assert.Equal(t, "labels", tagsMapping["value"])
|
||||
assert.Equal(t, "/(.*)/", tagsMapping["regex"])
|
||||
|
||||
textMapping, ok := result["text"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", textMapping["source"])
|
||||
assert.Equal(t, "constant", textMapping["value"])
|
||||
})
|
||||
}
|
||||
@@ -113,6 +113,11 @@ func convertAnnotationQuery_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardAnnota
|
||||
out.Spec.Filter = (*dashv2beta1.DashboardAnnotationPanelFilter)(in.Spec.Filter)
|
||||
out.Spec.LegacyOptions = in.Spec.LegacyOptions
|
||||
|
||||
// Convert mappings
|
||||
if in.Spec.Mappings != nil {
|
||||
out.Spec.Mappings = convertAnnotationMappings_V2alpha1_to_V2beta1(in.Spec.Mappings)
|
||||
}
|
||||
|
||||
// Convert query - move datasource from annotation spec to query
|
||||
if err := convertDataQuery_V2alpha1_to_V2beta1(in.Spec.Query, &out.Spec.Query, in.Spec.Datasource, scope); err != nil {
|
||||
return err
|
||||
@@ -982,3 +987,18 @@ func convertRowLayout_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardGridLayoutKi
|
||||
func convertTabLayout_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, out *dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, scope conversion.Scope) error {
|
||||
return convertLayout_V2alpha1_to_V2beta1(in, out, scope)
|
||||
}
|
||||
|
||||
func convertAnnotationMappings_V2alpha1_to_V2beta1(in map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping) map[string]dashv2beta1.DashboardAnnotationEventFieldMapping {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]dashv2beta1.DashboardAnnotationEventFieldMapping, len(in))
|
||||
for key, mapping := range in {
|
||||
out[key] = dashv2beta1.DashboardAnnotationEventFieldMapping{
|
||||
Source: mapping.Source,
|
||||
Value: mapping.Value,
|
||||
Regex: mapping.Regex,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -115,6 +115,83 @@ func TestV2alpha1ToV2beta1(t *testing.T) {
|
||||
assert.True(t, variable.SwitchVariableKind.Spec.SkipUrlSync)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "annotation query with mappings",
|
||||
createV2alpha1: func() *dashv2alpha1.Dashboard {
|
||||
sourceField := "field"
|
||||
sourceText := "text"
|
||||
valueService := "service"
|
||||
valueConstant := "constant text"
|
||||
regexPattern := "/(.*)/"
|
||||
return &dashv2alpha1.Dashboard{
|
||||
Spec: dashv2alpha1.DashboardSpec{
|
||||
Title: "Test Dashboard",
|
||||
Annotations: []dashv2alpha1.DashboardAnnotationQueryKind{
|
||||
{
|
||||
Kind: "AnnotationQuery",
|
||||
Spec: dashv2alpha1.DashboardAnnotationQuerySpec{
|
||||
Name: "Test Annotation",
|
||||
Enable: true,
|
||||
Hide: false,
|
||||
IconColor: "red",
|
||||
Query: &dashv2alpha1.DashboardDataQueryKind{
|
||||
Kind: "prometheus",
|
||||
Spec: map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
},
|
||||
Mappings: map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: &sourceField,
|
||||
Value: &valueService,
|
||||
},
|
||||
"text": {
|
||||
Source: &sourceText,
|
||||
Value: &valueConstant,
|
||||
},
|
||||
"tags": {
|
||||
Source: &sourceField,
|
||||
Value: &valueService,
|
||||
Regex: ®exPattern,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
validateV2beta1: func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) {
|
||||
require.Len(t, v2beta1.Spec.Annotations, 1)
|
||||
annotation := v2beta1.Spec.Annotations[0]
|
||||
assert.Equal(t, "Test Annotation", annotation.Spec.Name)
|
||||
|
||||
// Verify mappings are preserved
|
||||
require.NotNil(t, annotation.Spec.Mappings)
|
||||
assert.Len(t, annotation.Spec.Mappings, 3)
|
||||
|
||||
// Check title mapping
|
||||
titleMapping, ok := annotation.Spec.Mappings["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
assert.Nil(t, titleMapping.Regex)
|
||||
|
||||
// Check text mapping
|
||||
textMapping, ok := annotation.Spec.Mappings["text"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", *textMapping.Source)
|
||||
assert.Equal(t, "constant text", *textMapping.Value)
|
||||
assert.Nil(t, textMapping.Regex)
|
||||
|
||||
// Check tags mapping
|
||||
tagsMapping, ok := annotation.Spec.Mappings["tags"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *tagsMapping.Source)
|
||||
assert.Equal(t, "service", *tagsMapping.Value)
|
||||
assert.Equal(t, "/(.*)/", *tagsMapping.Regex)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -114,6 +114,11 @@ func convertAnnotationQuery_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardAnnotat
|
||||
out.Spec.Filter = (*dashv2alpha1.DashboardAnnotationPanelFilter)(in.Spec.Filter)
|
||||
out.Spec.LegacyOptions = in.Spec.LegacyOptions
|
||||
|
||||
// Convert mappings
|
||||
if in.Spec.Mappings != nil {
|
||||
out.Spec.Mappings = convertAnnotationMappings_V2beta1_to_V2alpha1(in.Spec.Mappings)
|
||||
}
|
||||
|
||||
// Convert query - move datasource from query back to annotation spec
|
||||
query, datasource, err := convertDataQuery_V2beta1_to_V2alpha1(&in.Spec.Query, scope)
|
||||
if err != nil {
|
||||
@@ -1023,3 +1028,18 @@ func convertRowLayout_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardGridLayoutKin
|
||||
func convertTabLayout_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, out *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, scope conversion.Scope) error {
|
||||
return convertLayout_V2beta1_to_V2alpha1(in, out, scope)
|
||||
}
|
||||
|
||||
func convertAnnotationMappings_V2beta1_to_V2alpha1(in map[string]dashv2beta1.DashboardAnnotationEventFieldMapping) map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping, len(in))
|
||||
for key, mapping := range in {
|
||||
out[key] = dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
Source: mapping.Source,
|
||||
Value: mapping.Value,
|
||||
Regex: mapping.Regex,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -586,6 +586,85 @@ func TestV2beta1ToV2alpha1(t *testing.T) {
|
||||
assert.False(t, *row.Spec.Collapse)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "annotation query with mappings",
|
||||
createV2beta1: func() *dashv2beta1.Dashboard {
|
||||
sourceField := "field"
|
||||
sourceText := "text"
|
||||
valueService := "service"
|
||||
valueConstant := "constant text"
|
||||
regexPattern := "/(.*)/"
|
||||
return &dashv2beta1.Dashboard{
|
||||
Spec: dashv2beta1.DashboardSpec{
|
||||
Title: "Test Dashboard",
|
||||
Annotations: []dashv2beta1.DashboardAnnotationQueryKind{
|
||||
{
|
||||
Kind: "AnnotationQuery",
|
||||
Spec: dashv2beta1.DashboardAnnotationQuerySpec{
|
||||
Name: "Test Annotation",
|
||||
Enable: true,
|
||||
Hide: false,
|
||||
IconColor: "red",
|
||||
Query: dashv2beta1.DashboardDataQueryKind{
|
||||
Kind: "DataQuery",
|
||||
Group: "prometheus",
|
||||
Version: "v0",
|
||||
Spec: map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
},
|
||||
Mappings: map[string]dashv2beta1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: &sourceField,
|
||||
Value: &valueService,
|
||||
},
|
||||
"text": {
|
||||
Source: &sourceText,
|
||||
Value: &valueConstant,
|
||||
},
|
||||
"tags": {
|
||||
Source: &sourceField,
|
||||
Value: &valueService,
|
||||
Regex: ®exPattern,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) {
|
||||
require.Len(t, v2alpha1.Spec.Annotations, 1)
|
||||
annotation := v2alpha1.Spec.Annotations[0]
|
||||
assert.Equal(t, "Test Annotation", annotation.Spec.Name)
|
||||
|
||||
// Verify mappings are preserved
|
||||
require.NotNil(t, annotation.Spec.Mappings)
|
||||
assert.Len(t, annotation.Spec.Mappings, 3)
|
||||
|
||||
// Check title mapping
|
||||
titleMapping, ok := annotation.Spec.Mappings["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
assert.Nil(t, titleMapping.Regex)
|
||||
|
||||
// Check text mapping
|
||||
textMapping, ok := annotation.Spec.Mappings["text"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", *textMapping.Source)
|
||||
assert.Equal(t, "constant text", *textMapping.Value)
|
||||
assert.Nil(t, textMapping.Regex)
|
||||
|
||||
// Check tags mapping
|
||||
tagsMapping, ok := annotation.Spec.Mappings["tags"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *tagsMapping.Source)
|
||||
assert.Equal(t, "service", *tagsMapping.Value)
|
||||
assert.Equal(t, "/(.*)/", *tagsMapping.Regex)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
Reference in New Issue
Block a user