Compare commits

...

1 Commits

Author SHA1 Message Date
Ivan Ortega
8edfdff1fa Poc: Defaults 2025-12-16 11:15:27 +01:00
5 changed files with 377 additions and 2 deletions

View File

@@ -0,0 +1,74 @@
package v2alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
)
// SetDefaults_Dashboard ensures all panel queries have unique refIds
// This is called by the Kubernetes defaulting mechanism when dashboards are returned
func SetDefaults_Dashboard(obj *Dashboard) {
EnsureUniqueRefIds(&obj.Spec)
}
// EnsureUniqueRefIds ensures all queries within each panel have unique refIds
// This matches the frontend behavior in PanelModel.ensureQueryIds()
func EnsureUniqueRefIds(spec *DashboardSpec) {
for _, element := range spec.Elements {
if element.PanelKind != nil {
ensureUniqueRefIdsForPanel(element.PanelKind)
}
}
}
func ensureUniqueRefIdsForPanel(panel *DashboardPanelKind) {
queries := panel.Spec.Data.Spec.Queries
if len(queries) == 0 {
return
}
// First pass: collect existing refIds
existingRefIds := make(map[string]bool)
for i := range queries {
if queries[i].Spec.RefId != "" {
existingRefIds[queries[i].Spec.RefId] = true
}
}
// Second pass: assign unique refIds to queries without one
for i := range queries {
if queries[i].Spec.RefId == "" {
queries[i].Spec.RefId = getNextRefId(existingRefIds)
existingRefIds[queries[i].Spec.RefId] = true
}
}
}
// getNextRefId generates the next available refId (A, B, C, ..., Z, AA, AB, etc.)
// This matches the frontend behavior in packages/grafana-data/src/query/refId.ts
func getNextRefId(existingRefIds map[string]bool) string {
for num := 0; ; num++ {
refId := getRefIdFromNumber(num)
if !existingRefIds[refId] {
return refId
}
}
}
// getRefIdFromNumber converts a number to a refId (0=A, 1=B, ..., 25=Z, 26=AA, 27=AB, etc.)
func getRefIdFromNumber(num int) string {
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
if num < len(letters) {
return string(letters[num])
}
return getRefIdFromNumber(num/len(letters)-1) + string(letters[num%len(letters)])
}
// RegisterCustomDefaults registers custom defaulting functions for Dashboard types.
// This should be called from RegisterDefaults in zz_generated.defaults.go
// However, since that file is auto-generated, we provide this as a separate registration
func RegisterCustomDefaults(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&Dashboard{}, func(obj interface{}) {
SetDefaults_Dashboard(obj.(*Dashboard))
})
return nil
}

View File

@@ -99,5 +99,9 @@ func addKnownTypes(scheme *runtime.Scheme) error {
}
func addDefaultingFuncs(scheme *runtime.Scheme) error {
return RegisterDefaults(scheme)
if err := RegisterDefaults(scheme); err != nil {
return err
}
// Register custom defaults to ensure unique refIds in panel queries
return RegisterCustomDefaults(scheme)
}

View File

@@ -0,0 +1,74 @@
package v2beta1
import (
"k8s.io/apimachinery/pkg/runtime"
)
// SetDefaults_Dashboard ensures all panel queries have unique refIds
// This is called by the Kubernetes defaulting mechanism when dashboards are returned
func SetDefaults_Dashboard(obj *Dashboard) {
EnsureUniqueRefIds(&obj.Spec)
}
// EnsureUniqueRefIds ensures all queries within each panel have unique refIds
// This matches the frontend behavior in PanelModel.ensureQueryIds()
func EnsureUniqueRefIds(spec *DashboardSpec) {
for _, element := range spec.Elements {
if element.PanelKind != nil {
ensureUniqueRefIdsForPanel(element.PanelKind)
}
}
}
func ensureUniqueRefIdsForPanel(panel *DashboardPanelKind) {
queries := panel.Spec.Data.Spec.Queries
if len(queries) == 0 {
return
}
// First pass: collect existing refIds
existingRefIds := make(map[string]bool)
for i := range queries {
if queries[i].Spec.RefId != "" {
existingRefIds[queries[i].Spec.RefId] = true
}
}
// Second pass: assign unique refIds to queries without one
for i := range queries {
if queries[i].Spec.RefId == "" {
queries[i].Spec.RefId = getNextRefId(existingRefIds)
existingRefIds[queries[i].Spec.RefId] = true
}
}
}
// getNextRefId generates the next available refId (A, B, C, ..., Z, AA, AB, etc.)
// This matches the frontend behavior in packages/grafana-data/src/query/refId.ts
func getNextRefId(existingRefIds map[string]bool) string {
for num := 0; ; num++ {
refId := getRefIdFromNumber(num)
if !existingRefIds[refId] {
return refId
}
}
}
// getRefIdFromNumber converts a number to a refId (0=A, 1=B, ..., 25=Z, 26=AA, 27=AB, etc.)
func getRefIdFromNumber(num int) string {
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
if num < len(letters) {
return string(letters[num])
}
return getRefIdFromNumber(num/len(letters)-1) + string(letters[num%len(letters)])
}
// RegisterCustomDefaults registers custom defaulting functions for Dashboard types.
// This should be called from RegisterDefaults in zz_generated.defaults.go
// However, since that file is auto-generated, we provide this as a separate registration
func RegisterCustomDefaults(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&Dashboard{}, func(obj interface{}) {
SetDefaults_Dashboard(obj.(*Dashboard))
})
return nil
}

View File

@@ -0,0 +1,219 @@
package v2beta1
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetRefIdFromNumber(t *testing.T) {
testCases := []struct {
num int
expected string
}{
{0, "A"},
{1, "B"},
{25, "Z"},
{26, "AA"},
{27, "AB"},
{51, "AZ"},
{52, "BA"},
{701, "ZZ"},
{702, "AAA"},
}
for _, tc := range testCases {
t.Run(tc.expected, func(t *testing.T) {
result := getRefIdFromNumber(tc.num)
assert.Equal(t, tc.expected, result, "getRefIdFromNumber(%d) should return %s", tc.num, tc.expected)
})
}
}
func TestGetNextRefId(t *testing.T) {
testCases := []struct {
name string
existing map[string]bool
expected string
}{
{
name: "empty map returns A",
existing: map[string]bool{},
expected: "A",
},
{
name: "A exists returns B",
existing: map[string]bool{"A": true},
expected: "B",
},
{
name: "A and B exist returns C",
existing: map[string]bool{"A": true, "B": true},
expected: "C",
},
{
name: "gap in sequence returns first available",
existing: map[string]bool{"A": true, "C": true, "D": true},
expected: "B",
},
{
name: "A-Z exist returns AA",
existing: func() map[string]bool {
m := make(map[string]bool)
for i := 0; i < 26; i++ {
m[string(rune('A'+i))] = true
}
return m
}(),
expected: "AA",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := getNextRefId(tc.existing)
assert.Equal(t, tc.expected, result)
})
}
}
func TestEnsureUniqueRefIds(t *testing.T) {
t.Run("assigns unique refIds to queries without refIds", func(t *testing.T) {
spec := &DashboardSpec{
Elements: map[string]DashboardElement{
"panel-1": {
PanelKind: &DashboardPanelKind{
Kind: "Panel",
Spec: DashboardPanelSpec{
Data: DashboardQueryGroupKind{
Spec: DashboardQueryGroupSpec{
Queries: []DashboardPanelQueryKind{
{Spec: DashboardPanelQuerySpec{RefId: ""}},
{Spec: DashboardPanelQuerySpec{RefId: ""}},
{Spec: DashboardPanelQuerySpec{RefId: ""}},
},
},
},
},
},
},
},
}
EnsureUniqueRefIds(spec)
panel := spec.Elements["panel-1"].PanelKind
require.NotNil(t, panel)
require.Len(t, panel.Spec.Data.Spec.Queries, 3)
assert.Equal(t, "A", panel.Spec.Data.Spec.Queries[0].Spec.RefId)
assert.Equal(t, "B", panel.Spec.Data.Spec.Queries[1].Spec.RefId)
assert.Equal(t, "C", panel.Spec.Data.Spec.Queries[2].Spec.RefId)
})
t.Run("preserves existing refIds and fills gaps", func(t *testing.T) {
spec := &DashboardSpec{
Elements: map[string]DashboardElement{
"panel-1": {
PanelKind: &DashboardPanelKind{
Kind: "Panel",
Spec: DashboardPanelSpec{
Data: DashboardQueryGroupKind{
Spec: DashboardQueryGroupSpec{
Queries: []DashboardPanelQueryKind{
{Spec: DashboardPanelQuerySpec{RefId: "A"}},
{Spec: DashboardPanelQuerySpec{RefId: ""}},
{Spec: DashboardPanelQuerySpec{RefId: "D"}},
{Spec: DashboardPanelQuerySpec{RefId: ""}},
},
},
},
},
},
},
},
}
EnsureUniqueRefIds(spec)
panel := spec.Elements["panel-1"].PanelKind
require.NotNil(t, panel)
require.Len(t, panel.Spec.Data.Spec.Queries, 4)
assert.Equal(t, "A", panel.Spec.Data.Spec.Queries[0].Spec.RefId)
assert.Equal(t, "B", panel.Spec.Data.Spec.Queries[1].Spec.RefId)
assert.Equal(t, "D", panel.Spec.Data.Spec.Queries[2].Spec.RefId)
assert.Equal(t, "C", panel.Spec.Data.Spec.Queries[3].Spec.RefId)
})
t.Run("handles library panels (no modification)", func(t *testing.T) {
spec := &DashboardSpec{
Elements: map[string]DashboardElement{
"panel-1": {
LibraryPanelKind: &DashboardLibraryPanelKind{
Kind: "LibraryPanel",
Spec: DashboardLibraryPanelKindSpec{
LibraryPanel: DashboardLibraryPanelRef{
Uid: "lib-uid",
Name: "lib-name",
},
},
},
},
},
}
// Should not panic
EnsureUniqueRefIds(spec)
})
t.Run("handles multiple panels", func(t *testing.T) {
spec := &DashboardSpec{
Elements: map[string]DashboardElement{
"panel-1": {
PanelKind: &DashboardPanelKind{
Kind: "Panel",
Spec: DashboardPanelSpec{
Data: DashboardQueryGroupKind{
Spec: DashboardQueryGroupSpec{
Queries: []DashboardPanelQueryKind{
{Spec: DashboardPanelQuerySpec{RefId: ""}},
{Spec: DashboardPanelQuerySpec{RefId: ""}},
},
},
},
},
},
},
"panel-2": {
PanelKind: &DashboardPanelKind{
Kind: "Panel",
Spec: DashboardPanelSpec{
Data: DashboardQueryGroupKind{
Spec: DashboardQueryGroupSpec{
Queries: []DashboardPanelQueryKind{
{Spec: DashboardPanelQuerySpec{RefId: ""}},
{Spec: DashboardPanelQuerySpec{RefId: ""}},
},
},
},
},
},
},
},
}
EnsureUniqueRefIds(spec)
// Each panel should have unique refIds independently
panel1 := spec.Elements["panel-1"].PanelKind
panel2 := spec.Elements["panel-2"].PanelKind
require.NotNil(t, panel1)
require.NotNil(t, panel2)
assert.Equal(t, "A", panel1.Spec.Data.Spec.Queries[0].Spec.RefId)
assert.Equal(t, "B", panel1.Spec.Data.Spec.Queries[1].Spec.RefId)
assert.Equal(t, "A", panel2.Spec.Data.Spec.Queries[0].Spec.RefId)
assert.Equal(t, "B", panel2.Spec.Data.Spec.Queries[1].Spec.RefId)
})
}

View File

@@ -99,5 +99,9 @@ func addKnownTypes(scheme *runtime.Scheme) error {
}
func addDefaultingFuncs(scheme *runtime.Scheme) error {
return RegisterDefaults(scheme)
if err := RegisterDefaults(scheme); err != nil {
return err
}
// Register custom defaults to ensure unique refIds in panel queries
return RegisterCustomDefaults(scheme)
}