K8s/Dashboards: Generate Dashboards k8s APIs using Grafana App SDK (#99966)

* Generate Dashboard kinds with `grafana-app-sdk`

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

* Hack together a fix for invalid TS codegen for v0 & v1

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

* Address Go linter issues

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

* Address TS linter issues

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

* Add new app to CODEOWNERS

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

* Fix a couple of issues detected by tests

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

* Update OpenAPI definitions and test files

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

* Remove title from Dashboard v1alpha1 spec

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

* Remove unused CUE schemas

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

* remove unrelated files

* allow any in the generated betterer

* Add a comment explaining why we don't use deepcopy-gen

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

* Default to v2alpha1 if dashboards v2 FF is enabled

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

---------

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Igor Suleymanov
2025-03-11 13:00:37 +02:00
committed by GitHub
co-authored by Ryan McKinley
parent 3fffb2872e
commit ea89a68028
81 changed files with 11452 additions and 679 deletions
+9 -11
View File
@@ -1,6 +1,7 @@
package dashboard
import (
"encoding/json"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
@@ -35,7 +36,11 @@ func NewDashboardLargeObjectSupport(scheme *runtime.Scheme) *apistore.BasicLarge
case *dashboardV1.Dashboard:
reduceUnstructredSpec(&dash.Spec)
case *dashboardV2.Dashboard:
reduceUnstructredSpec(&dash.Spec)
dash.Spec = dashboardV2.DashboardSpec{
Title: dash.Spec.Title,
Description: dash.Spec.Description,
Tags: dash.Spec.Tags,
}
default:
return fmt.Errorf("unsupported dashboard type %T", obj)
}
@@ -45,23 +50,16 @@ func NewDashboardLargeObjectSupport(scheme *runtime.Scheme) *apistore.BasicLarge
},
RebuildSpec: func(obj runtime.Object, blob []byte) error {
body := commonV0.Unstructured{}
err := body.UnmarshalJSON(blob)
if err != nil {
return err
}
switch dash := obj.(type) {
case *dashboardV0.Dashboard:
dash.Spec = body
return dash.Spec.UnmarshalJSON(blob)
case *dashboardV1.Dashboard:
dash.Spec = body
return dash.Spec.UnmarshalJSON(blob)
case *dashboardV2.Dashboard:
dash.Spec = body
return json.Unmarshal(blob, &dash.Spec)
default:
return fmt.Errorf("unsupported dashboard type %T", obj)
}
return nil
},
}
}
+1 -4
View File
@@ -38,10 +38,7 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute
internalID = int64(id)
}
case *dashboardV2.Dashboard:
if id, ok := v.Spec.Object["id"].(float64); ok {
delete(v.Spec.Object, "id")
internalID = int64(id)
}
// Noop for V2
default:
return fmt.Errorf("mutation error: expected to dashboard, got %T", obj)
}
+10
View File
@@ -109,6 +109,16 @@ func RegisterAPIService(
}
func (b *DashboardsAPIBuilder) GetGroupVersions() []schema.GroupVersion {
if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagUseV2DashboardsAPI) {
// If dashboards v2 is enabled, we want to use v2alpha1 as the default API version.
return []schema.GroupVersion{
dashboardv2alpha1.DashboardResourceInfo.GroupVersion(),
dashboardv0alpha1.DashboardResourceInfo.GroupVersion(),
dashboardv1alpha1.DashboardResourceInfo.GroupVersion(),
}
}
// TODO (@radiohead): should we switch to v1alpha1 by default?
return []schema.GroupVersion{
dashboardv0alpha1.DashboardResourceInfo.GroupVersion(),
dashboardv1alpha1.DashboardResourceInfo.GroupVersion(),
@@ -7,11 +7,15 @@ import (
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/apis/dashboard/v1alpha1"
"github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
)
@@ -158,3 +162,93 @@ func TestDashboardAPIBuilder_Validate(t *testing.T) {
})
}
}
func TestDashboardAPIBuilder_GetGroupVersions(t *testing.T) {
tests := []struct {
name string
enabledFeatures []string
expected []schema.GroupVersion
}{
{
name: "should return v0alpha1 by default",
enabledFeatures: []string{},
expected: []schema.GroupVersion{
v0alpha1.DashboardResourceInfo.GroupVersion(),
v1alpha1.DashboardResourceInfo.GroupVersion(),
v2alpha1.DashboardResourceInfo.GroupVersion(),
},
},
{
name: "should return v0alpha1 as the default if some other feature is enabled",
enabledFeatures: []string{
featuremgmt.FlagKubernetesDashboards,
},
expected: []schema.GroupVersion{
v0alpha1.DashboardResourceInfo.GroupVersion(),
v1alpha1.DashboardResourceInfo.GroupVersion(),
v2alpha1.DashboardResourceInfo.GroupVersion(),
},
},
{
name: "should return v2alpha1 as the default if dashboards v2 is enabled",
enabledFeatures: []string{
featuremgmt.FlagUseV2DashboardsAPI,
},
expected: []schema.GroupVersion{
v2alpha1.DashboardResourceInfo.GroupVersion(),
v0alpha1.DashboardResourceInfo.GroupVersion(),
v1alpha1.DashboardResourceInfo.GroupVersion(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
builder := &DashboardsAPIBuilder{
features: newMockFeatureToggles(t, tt.enabledFeatures...),
}
require.Equal(t, tt.expected, builder.GetGroupVersions())
})
}
}
type mockFeatureToggles struct {
// We need to make a copy in `GetEnabled` anyway,
// so no need to store the original map as map[string]bool.
enabledFeatures map[string]struct{}
}
func newMockFeatureToggles(t *testing.T, enabledFeatures ...string) featuremgmt.FeatureToggles {
t.Helper()
res := &mockFeatureToggles{
enabledFeatures: make(map[string]struct{}, len(enabledFeatures)),
}
for _, f := range enabledFeatures {
res.enabledFeatures[f] = struct{}{}
}
return res
}
func (m *mockFeatureToggles) IsEnabledGlobally(feature string) bool {
_, ok := m.enabledFeatures[feature]
return ok
}
func (m *mockFeatureToggles) IsEnabled(ctx context.Context, feature string) bool {
_, ok := m.enabledFeatures[feature]
return ok
}
func (m *mockFeatureToggles) GetEnabled(ctx context.Context) map[string]bool {
res := make(map[string]bool, len(m.enabledFeatures))
for f := range m.enabledFeatures {
res[f] = true
}
return res
}