Dashboard: Multi-version builder (#100305)

This commit is contained in:
Todd Treece
2025-02-21 06:50:29 -05:00
committed by GitHub
parent 7be1fd953a
commit 3992ac2ac1
31 changed files with 480 additions and 1621 deletions

View File

@ -16,7 +16,12 @@ type Dashboard struct {
metav1.ObjectMeta `json:"metadata,omitempty"`
// The dashboard body (unstructured for now)
Spec common.Unstructured `json:"spec"`
Spec DashboardSpec `json:"spec"`
}
type DashboardSpec struct {
Title string `json:"title"`
common.Unstructured `json:",inline"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View File

@ -1,22 +1,31 @@
package v1alpha1
package v0alpha1
import (
conversion "k8s.io/apimachinery/pkg/conversion"
klog "k8s.io/klog/v2"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard"
"github.com/grafana/grafana/pkg/apis/dashboard/migration"
"github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion"
)
func Convert_v0alpha1_Unstructured_To_v1alpha1_DashboardSpec(in *common.Unstructured, out *DashboardSpec, s conversion.Scope) error {
func Convert_dashboard_DashboardSpec_To_v0alpha1_Unstructured(in *dashboard.DashboardSpec, out *common.Unstructured, s conversion.Scope) error {
*out = in.Unstructured
if in.Title != "" {
out.Object["title"] = in.Title
}
return nil
}
func Convert_v0alpha1_Unstructured_To_dashboard_DashboardSpec(in *common.Unstructured, out *dashboard.DashboardSpec, s conversion.Scope) error {
out.Unstructured = *in
err := migration.Migrate(out.Unstructured.Object, schemaversion.LATEST_VERSION)
if err != nil {
return err
}
t, ok := out.Unstructured.Object["title"].(string)
t, ok := out.Object["title"].(string)
if !ok {
klog.V(5).Infof("unstructured dashboard title field is not a string %v", t)
return nil // skip setting the title if it's not a string in the unstructured object
@ -24,11 +33,3 @@ func Convert_v0alpha1_Unstructured_To_v1alpha1_DashboardSpec(in *common.Unstruct
out.Title = t
return nil
}
func Convert_v1alpha1_DashboardSpec_To_v0alpha1_Unstructured(in *DashboardSpec, out *common.Unstructured, s conversion.Scope) error {
*out = in.Unstructured
if in.Title != "" {
out.Object["title"] = in.Title
}
return nil
}

View File

@ -1,4 +1,4 @@
package v1alpha1
package v0alpha1
import (
"encoding/json"
@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apis/dashboard"
)
func TestConvertDashboardVersions(t *testing.T) {
@ -43,7 +44,7 @@ func TestConvertDashboardVersions(t *testing.T) {
},
"timepicker": {},
"timezone": "utc",
"title": "New dashboard",
"title": "New",
"uid": "be3ymutzclgqod",
"version": 1,
"weekStart": ""
@ -51,17 +52,17 @@ func TestConvertDashboardVersions(t *testing.T) {
object := common.Unstructured{}
err := json.Unmarshal(dashboardV0Spec, &object.Object)
require.NoError(t, err)
result := DashboardSpec{}
// convert v0 to v1, where we should extract the title & all other elements should be copied
err = Convert_v0alpha1_Unstructured_To_v1alpha1_DashboardSpec(&object, &result, nil)
in := dashboard.DashboardSpec{Title: "New dashboard", Unstructured: object}
result := common.Unstructured{}
err = Convert_dashboard_DashboardSpec_To_v0alpha1_Unstructured(&in, &result, nil)
require.NoError(t, err)
require.Equal(t, result.Title, "New dashboard")
require.Equal(t, result.Unstructured, object)
require.Equal(t, result.Unstructured.Object["refresh"], "", "schemaVersion migration not applied. refresh should be an empty string")
// now convert back & ensure it is the same
object2 := common.Unstructured{}
err = Convert_v1alpha1_DashboardSpec_To_v0alpha1_Unstructured(&result, &object2, nil)
object2 := *(result.DeepCopy())
result2 := dashboard.DashboardSpec{}
err = Convert_v0alpha1_Unstructured_To_dashboard_DashboardSpec(&object2, &result2, nil)
require.NoError(t, err)
require.Equal(t, object, object2)
require.Equal(t, result2.Title, "New dashboard")
require.Equal(t, result2.Unstructured.Object["schemaVersion"], 41, "schemaVersion migration not applied.")
require.Equal(t, result2.Unstructured.Object["refresh"], "", "schemaVersion migration not applied. refresh should be an empty string")
}

View File

@ -12,6 +12,7 @@ import (
unsafe "unsafe"
datav0alpha1 "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
commonv0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
@ -159,6 +160,16 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddConversionFunc((*dashboard.DashboardSpec)(nil), (*commonv0alpha1.Unstructured)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_dashboard_DashboardSpec_To_v0alpha1_Unstructured(a.(*dashboard.DashboardSpec), b.(*commonv0alpha1.Unstructured), scope)
}); err != nil {
return err
}
if err := s.AddConversionFunc((*commonv0alpha1.Unstructured)(nil), (*dashboard.DashboardSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v0alpha1_Unstructured_To_dashboard_DashboardSpec(a.(*commonv0alpha1.Unstructured), b.(*dashboard.DashboardSpec), scope)
}); err != nil {
return err
}
return nil
}
@ -218,7 +229,9 @@ func Convert_dashboard_AnnotationPermission_To_v0alpha1_AnnotationPermission(in
func autoConvert_v0alpha1_Dashboard_To_dashboard_Dashboard(in *Dashboard, out *dashboard.Dashboard, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.Spec = in.Spec
if err := Convert_v0alpha1_Unstructured_To_dashboard_DashboardSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
}
@ -229,7 +242,9 @@ func Convert_v0alpha1_Dashboard_To_dashboard_Dashboard(in *Dashboard, out *dashb
func autoConvert_dashboard_Dashboard_To_v0alpha1_Dashboard(in *dashboard.Dashboard, out *Dashboard, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.Spec = in.Spec
if err := Convert_dashboard_DashboardSpec_To_v0alpha1_Unstructured(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
}
@ -274,7 +289,17 @@ func Convert_dashboard_DashboardAccess_To_v0alpha1_DashboardAccess(in *dashboard
func autoConvert_v0alpha1_DashboardList_To_dashboard_DashboardList(in *DashboardList, out *dashboard.DashboardList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
out.Items = *(*[]dashboard.Dashboard)(unsafe.Pointer(&in.Items))
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]dashboard.Dashboard, len(*in))
for i := range *in {
if err := Convert_v0alpha1_Dashboard_To_dashboard_Dashboard(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
return nil
}
@ -285,7 +310,17 @@ func Convert_v0alpha1_DashboardList_To_dashboard_DashboardList(in *DashboardList
func autoConvert_dashboard_DashboardList_To_v0alpha1_DashboardList(in *dashboard.DashboardList, out *DashboardList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
out.Items = *(*[]Dashboard)(unsafe.Pointer(&in.Items))
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Dashboard, len(*in))
for i := range *in {
if err := Convert_dashboard_Dashboard_To_v0alpha1_Dashboard(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
return nil
}

View File

@ -11,8 +11,7 @@ import (
url "net/url"
unsafe "unsafe"
datav0alpha1 "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
v0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
v0alpha1 "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
@ -75,6 +74,16 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*DashboardSpec)(nil), (*dashboard.DashboardSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_DashboardSpec_To_dashboard_DashboardSpec(a.(*DashboardSpec), b.(*dashboard.DashboardSpec), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*dashboard.DashboardSpec)(nil), (*DashboardSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_dashboard_DashboardSpec_To_v1alpha1_DashboardSpec(a.(*dashboard.DashboardSpec), b.(*DashboardSpec), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*DashboardVersionInfo)(nil), (*dashboard.DashboardVersionInfo)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_DashboardVersionInfo_To_dashboard_DashboardVersionInfo(a.(*DashboardVersionInfo), b.(*dashboard.DashboardVersionInfo), scope)
}); err != nil {
@ -160,16 +169,6 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddConversionFunc((*v0alpha1.Unstructured)(nil), (*DashboardSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v0alpha1_Unstructured_To_v1alpha1_DashboardSpec(a.(*v0alpha1.Unstructured), b.(*DashboardSpec), scope)
}); err != nil {
return err
}
if err := s.AddConversionFunc((*DashboardSpec)(nil), (*v0alpha1.Unstructured)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_DashboardSpec_To_v0alpha1_Unstructured(a.(*DashboardSpec), b.(*v0alpha1.Unstructured), scope)
}); err != nil {
return err
}
return nil
}
@ -229,7 +228,7 @@ func Convert_dashboard_AnnotationPermission_To_v1alpha1_AnnotationPermission(in
func autoConvert_v1alpha1_Dashboard_To_dashboard_Dashboard(in *Dashboard, out *dashboard.Dashboard, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_v1alpha1_DashboardSpec_To_v0alpha1_Unstructured(&in.Spec, &out.Spec, s); err != nil {
if err := Convert_v1alpha1_DashboardSpec_To_dashboard_DashboardSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
@ -242,7 +241,7 @@ func Convert_v1alpha1_Dashboard_To_dashboard_Dashboard(in *Dashboard, out *dashb
func autoConvert_dashboard_Dashboard_To_v1alpha1_Dashboard(in *dashboard.Dashboard, out *Dashboard, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_v0alpha1_Unstructured_To_v1alpha1_DashboardSpec(&in.Spec, &out.Spec, s); err != nil {
if err := Convert_dashboard_DashboardSpec_To_v1alpha1_DashboardSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
@ -289,17 +288,7 @@ func Convert_dashboard_DashboardAccess_To_v1alpha1_DashboardAccess(in *dashboard
func autoConvert_v1alpha1_DashboardList_To_dashboard_DashboardList(in *DashboardList, out *dashboard.DashboardList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]dashboard.Dashboard, len(*in))
for i := range *in {
if err := Convert_v1alpha1_Dashboard_To_dashboard_Dashboard(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
out.Items = *(*[]dashboard.Dashboard)(unsafe.Pointer(&in.Items))
return nil
}
@ -310,17 +299,7 @@ func Convert_v1alpha1_DashboardList_To_dashboard_DashboardList(in *DashboardList
func autoConvert_dashboard_DashboardList_To_v1alpha1_DashboardList(in *dashboard.DashboardList, out *DashboardList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Dashboard, len(*in))
for i := range *in {
if err := Convert_dashboard_Dashboard_To_v1alpha1_Dashboard(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
out.Items = *(*[]Dashboard)(unsafe.Pointer(&in.Items))
return nil
}
@ -329,6 +308,28 @@ func Convert_dashboard_DashboardList_To_v1alpha1_DashboardList(in *dashboard.Das
return autoConvert_dashboard_DashboardList_To_v1alpha1_DashboardList(in, out, s)
}
func autoConvert_v1alpha1_DashboardSpec_To_dashboard_DashboardSpec(in *DashboardSpec, out *dashboard.DashboardSpec, s conversion.Scope) error {
out.Title = in.Title
out.Unstructured = in.Unstructured
return nil
}
// Convert_v1alpha1_DashboardSpec_To_dashboard_DashboardSpec is an autogenerated conversion function.
func Convert_v1alpha1_DashboardSpec_To_dashboard_DashboardSpec(in *DashboardSpec, out *dashboard.DashboardSpec, s conversion.Scope) error {
return autoConvert_v1alpha1_DashboardSpec_To_dashboard_DashboardSpec(in, out, s)
}
func autoConvert_dashboard_DashboardSpec_To_v1alpha1_DashboardSpec(in *dashboard.DashboardSpec, out *DashboardSpec, s conversion.Scope) error {
out.Title = in.Title
out.Unstructured = in.Unstructured
return nil
}
// Convert_dashboard_DashboardSpec_To_v1alpha1_DashboardSpec is an autogenerated conversion function.
func Convert_dashboard_DashboardSpec_To_v1alpha1_DashboardSpec(in *dashboard.DashboardSpec, out *DashboardSpec, s conversion.Scope) error {
return autoConvert_dashboard_DashboardSpec_To_v1alpha1_DashboardSpec(in, out, s)
}
func autoConvert_v1alpha1_DashboardVersionInfo_To_dashboard_DashboardVersionInfo(in *DashboardVersionInfo, out *dashboard.DashboardVersionInfo, s conversion.Scope) error {
out.Version = in.Version
out.ParentVersion = in.ParentVersion
@ -466,8 +467,8 @@ func autoConvert_v1alpha1_LibraryPanelSpec_To_dashboard_LibraryPanelSpec(in *Lib
out.Description = in.Description
out.Options = in.Options
out.FieldConfig = in.FieldConfig
out.Datasource = (*datav0alpha1.DataSourceRef)(unsafe.Pointer(in.Datasource))
out.Targets = *(*[]datav0alpha1.DataQuery)(unsafe.Pointer(&in.Targets))
out.Datasource = (*v0alpha1.DataSourceRef)(unsafe.Pointer(in.Datasource))
out.Targets = *(*[]v0alpha1.DataQuery)(unsafe.Pointer(&in.Targets))
return nil
}
@ -483,8 +484,8 @@ func autoConvert_dashboard_LibraryPanelSpec_To_v1alpha1_LibraryPanelSpec(in *das
out.Description = in.Description
out.Options = in.Options
out.FieldConfig = in.FieldConfig
out.Datasource = (*datav0alpha1.DataSourceRef)(unsafe.Pointer(in.Datasource))
out.Targets = *(*[]datav0alpha1.DataQuery)(unsafe.Pointer(&in.Targets))
out.Datasource = (*v0alpha1.DataSourceRef)(unsafe.Pointer(in.Datasource))
out.Targets = *(*[]v0alpha1.DataQuery)(unsafe.Pointer(&in.Targets))
return nil
}

View File

@ -1,34 +0,0 @@
package v2alpha1
import (
conversion "k8s.io/apimachinery/pkg/conversion"
klog "k8s.io/klog/v2"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apis/dashboard/migration"
"github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion"
)
func Convert_v0alpha1_Unstructured_To_v2alpha1_DashboardSpec(in *common.Unstructured, out *DashboardSpec, s conversion.Scope) error {
out.Unstructured = *in
err := migration.Migrate(out.Unstructured.Object, schemaversion.LATEST_VERSION)
if err != nil {
return err
}
t, ok := out.Unstructured.Object["title"].(string)
if !ok {
klog.V(5).Infof("unstructured dashboard title field is not a string %v", t)
return nil // skip setting the title if it's not a string in the unstructured object
}
out.Title = t
return nil
}
func Convert_v2alpha1_DashboardSpec_To_v0alpha1_Unstructured(in *DashboardSpec, out *common.Unstructured, s conversion.Scope) error {
*out = in.Unstructured
if in.Title != "" {
out.Object["title"] = in.Title
}
return nil
}

View File

@ -1,67 +0,0 @@
package v2alpha1
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
func TestConvertDashboardVersions(t *testing.T) {
dashboardV0Spec := []byte(`{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"type": "dashboard"
}
]
},
"refresh": true,
"description": "",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 11711,
"links": [],
"panels": [],
"preload": false,
"schemaVersion": 39,
"tags": [],
"templating": {
"list": []
},
"timepicker": {},
"timezone": "utc",
"title": "New dashboard",
"uid": "be3ymutzclgqod",
"version": 1,
"weekStart": ""
}`)
object := common.Unstructured{}
err := json.Unmarshal(dashboardV0Spec, &object.Object)
require.NoError(t, err)
result := DashboardSpec{}
// convert v0 to v2, where we should extract the title & all other elements should be copied
err = Convert_v0alpha1_Unstructured_To_v2alpha1_DashboardSpec(&object, &result, nil)
require.NoError(t, err)
require.Equal(t, result.Title, "New dashboard")
require.Equal(t, result.Unstructured, object)
require.Equal(t, result.Unstructured.Object["refresh"], "", "schemaVersion migration not applied. refresh should be an empty string")
// now convert back & ensure it is the same
object2 := common.Unstructured{}
err = Convert_v2alpha1_DashboardSpec_To_v0alpha1_Unstructured(&result, &object2, nil)
require.NoError(t, err)
require.Equal(t, object, object2)
}

View File

@ -11,8 +11,7 @@ import (
url "net/url"
unsafe "unsafe"
datav0alpha1 "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
v0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
v0alpha1 "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
@ -75,6 +74,16 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*DashboardSpec)(nil), (*dashboard.DashboardSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v2alpha1_DashboardSpec_To_dashboard_DashboardSpec(a.(*DashboardSpec), b.(*dashboard.DashboardSpec), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*dashboard.DashboardSpec)(nil), (*DashboardSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_dashboard_DashboardSpec_To_v2alpha1_DashboardSpec(a.(*dashboard.DashboardSpec), b.(*DashboardSpec), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*DashboardVersionInfo)(nil), (*dashboard.DashboardVersionInfo)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v2alpha1_DashboardVersionInfo_To_dashboard_DashboardVersionInfo(a.(*DashboardVersionInfo), b.(*dashboard.DashboardVersionInfo), scope)
}); err != nil {
@ -160,16 +169,6 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddConversionFunc((*v0alpha1.Unstructured)(nil), (*DashboardSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v0alpha1_Unstructured_To_v2alpha1_DashboardSpec(a.(*v0alpha1.Unstructured), b.(*DashboardSpec), scope)
}); err != nil {
return err
}
if err := s.AddConversionFunc((*DashboardSpec)(nil), (*v0alpha1.Unstructured)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v2alpha1_DashboardSpec_To_v0alpha1_Unstructured(a.(*DashboardSpec), b.(*v0alpha1.Unstructured), scope)
}); err != nil {
return err
}
return nil
}
@ -229,7 +228,7 @@ func Convert_dashboard_AnnotationPermission_To_v2alpha1_AnnotationPermission(in
func autoConvert_v2alpha1_Dashboard_To_dashboard_Dashboard(in *Dashboard, out *dashboard.Dashboard, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_v2alpha1_DashboardSpec_To_v0alpha1_Unstructured(&in.Spec, &out.Spec, s); err != nil {
if err := Convert_v2alpha1_DashboardSpec_To_dashboard_DashboardSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
@ -242,7 +241,7 @@ func Convert_v2alpha1_Dashboard_To_dashboard_Dashboard(in *Dashboard, out *dashb
func autoConvert_dashboard_Dashboard_To_v2alpha1_Dashboard(in *dashboard.Dashboard, out *Dashboard, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_v0alpha1_Unstructured_To_v2alpha1_DashboardSpec(&in.Spec, &out.Spec, s); err != nil {
if err := Convert_dashboard_DashboardSpec_To_v2alpha1_DashboardSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
@ -289,17 +288,7 @@ func Convert_dashboard_DashboardAccess_To_v2alpha1_DashboardAccess(in *dashboard
func autoConvert_v2alpha1_DashboardList_To_dashboard_DashboardList(in *DashboardList, out *dashboard.DashboardList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]dashboard.Dashboard, len(*in))
for i := range *in {
if err := Convert_v2alpha1_Dashboard_To_dashboard_Dashboard(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
out.Items = *(*[]dashboard.Dashboard)(unsafe.Pointer(&in.Items))
return nil
}
@ -310,17 +299,7 @@ func Convert_v2alpha1_DashboardList_To_dashboard_DashboardList(in *DashboardList
func autoConvert_dashboard_DashboardList_To_v2alpha1_DashboardList(in *dashboard.DashboardList, out *DashboardList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Dashboard, len(*in))
for i := range *in {
if err := Convert_dashboard_Dashboard_To_v2alpha1_Dashboard(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
out.Items = *(*[]Dashboard)(unsafe.Pointer(&in.Items))
return nil
}
@ -329,6 +308,28 @@ func Convert_dashboard_DashboardList_To_v2alpha1_DashboardList(in *dashboard.Das
return autoConvert_dashboard_DashboardList_To_v2alpha1_DashboardList(in, out, s)
}
func autoConvert_v2alpha1_DashboardSpec_To_dashboard_DashboardSpec(in *DashboardSpec, out *dashboard.DashboardSpec, s conversion.Scope) error {
out.Title = in.Title
out.Unstructured = in.Unstructured
return nil
}
// Convert_v2alpha1_DashboardSpec_To_dashboard_DashboardSpec is an autogenerated conversion function.
func Convert_v2alpha1_DashboardSpec_To_dashboard_DashboardSpec(in *DashboardSpec, out *dashboard.DashboardSpec, s conversion.Scope) error {
return autoConvert_v2alpha1_DashboardSpec_To_dashboard_DashboardSpec(in, out, s)
}
func autoConvert_dashboard_DashboardSpec_To_v2alpha1_DashboardSpec(in *dashboard.DashboardSpec, out *DashboardSpec, s conversion.Scope) error {
out.Title = in.Title
out.Unstructured = in.Unstructured
return nil
}
// Convert_dashboard_DashboardSpec_To_v2alpha1_DashboardSpec is an autogenerated conversion function.
func Convert_dashboard_DashboardSpec_To_v2alpha1_DashboardSpec(in *dashboard.DashboardSpec, out *DashboardSpec, s conversion.Scope) error {
return autoConvert_dashboard_DashboardSpec_To_v2alpha1_DashboardSpec(in, out, s)
}
func autoConvert_v2alpha1_DashboardVersionInfo_To_dashboard_DashboardVersionInfo(in *DashboardVersionInfo, out *dashboard.DashboardVersionInfo, s conversion.Scope) error {
out.Version = in.Version
out.ParentVersion = in.ParentVersion
@ -466,8 +467,8 @@ func autoConvert_v2alpha1_LibraryPanelSpec_To_dashboard_LibraryPanelSpec(in *Lib
out.Description = in.Description
out.Options = in.Options
out.FieldConfig = in.FieldConfig
out.Datasource = (*datav0alpha1.DataSourceRef)(unsafe.Pointer(in.Datasource))
out.Targets = *(*[]datav0alpha1.DataQuery)(unsafe.Pointer(&in.Targets))
out.Datasource = (*v0alpha1.DataSourceRef)(unsafe.Pointer(in.Datasource))
out.Targets = *(*[]v0alpha1.DataQuery)(unsafe.Pointer(&in.Targets))
return nil
}
@ -483,8 +484,8 @@ func autoConvert_dashboard_LibraryPanelSpec_To_v2alpha1_LibraryPanelSpec(in *das
out.Description = in.Description
out.Options = in.Options
out.FieldConfig = in.FieldConfig
out.Datasource = (*datav0alpha1.DataSourceRef)(unsafe.Pointer(in.Datasource))
out.Targets = *(*[]datav0alpha1.DataQuery)(unsafe.Pointer(&in.Targets))
out.Datasource = (*v0alpha1.DataSourceRef)(unsafe.Pointer(in.Datasource))
out.Targets = *(*[]v0alpha1.DataQuery)(unsafe.Pointer(&in.Targets))
return nil
}

View File

@ -127,6 +127,23 @@ func (in *DashboardList) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardSpec) DeepCopyInto(out *DashboardSpec) {
*out = *in
in.Unstructured.DeepCopyInto(&out.Unstructured)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSpec.
func (in *DashboardSpec) DeepCopy() *DashboardSpec {
if in == nil {
return nil
}
out := new(DashboardSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardVersionInfo) DeepCopyInto(out *DashboardVersionInfo) {
*out = *in

View File

@ -3,9 +3,6 @@ package apiregistry
import (
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications"
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
dashboardv0alpha1 "github.com/grafana/grafana/pkg/registry/apis/dashboard/v0alpha1"
dashboardv1alpha1 "github.com/grafana/grafana/pkg/registry/apis/dashboard/v1alpha1"
dashboardv2alpha1 "github.com/grafana/grafana/pkg/registry/apis/dashboard/v2alpha1"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
@ -22,9 +19,6 @@ type Service struct{}
// and give each builder the chance to register itself with the main server
func ProvideRegistryServiceSink(
_ *dashboardinternal.DashboardsAPIBuilder,
_ *dashboardv0alpha1.DashboardsAPIBuilder,
_ *dashboardv1alpha1.DashboardsAPIBuilder,
_ *dashboardv2alpha1.DashboardsAPIBuilder,
_ *dashboardsnapshot.SnapshotsAPIBuilder,
_ *featuretoggle.FeatureFlagAPIBuilder,
_ *datasource.DataSourceAPIBuilder,

View File

@ -12,3 +12,7 @@ func ToInternalDashboard(scheme *runtime.Scheme, obj runtime.Object) (*dashboard
}
return dash, nil
}
func FromInternalDashboard(scheme *runtime.Scheme, dash *dashboard.Dashboard, obj runtime.Object) error {
return scheme.Convert(dash, obj, nil)
}

View File

@ -46,9 +46,7 @@ func TestConvertDashboardVersionsToInternal(t *testing.T) {
Labels: labels,
ResourceVersion: rv,
},
Spec: common.Unstructured{
Object: body,
},
Spec: dashboardinternal.DashboardSpec{Title: title, Unstructured: common.Unstructured{Object: body}},
}
dashV0 := &dashboardv0alpha1.Dashboard{
ObjectMeta: v1.ObjectMeta{

View File

@ -28,7 +28,7 @@ func NewDashboardLargeObjectSupport(scheme *runtime.Scheme) *apistore.BasicLarge
}
old := dash.Spec.Object
spec := commonV0.Unstructured{Object: make(map[string]any)}
dash.Spec = spec
dash.Spec = dashboard.DashboardSpec{Unstructured: spec}
dash.SetManagedFields(nil) // this could be bigger than the object!
keep := []string{"title", "description", "tags", "schemaVersion"}

View File

@ -53,7 +53,7 @@ func TestLargeDashboardSupport(t *testing.T) {
small, err := json.MarshalIndent(&dash.Spec, "", " ")
require.NoError(t, err)
require.JSONEq(t, `{
"schemaVersion": 33,
"schemaVersion": 41,
"title": "Panel tests - All panels",
"tags": ["gdev","panel-tests","all-panels"]
}`, string(small))

View File

@ -10,7 +10,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storage"
@ -25,20 +24,16 @@ type LatestConnector interface {
rest.StorageMetadata
}
func NewLatestConnector(unified resource.ResourceClient, gr schema.GroupResource, opts generic.RESTOptions, scheme *runtime.Scheme) LatestConnector {
func NewLatestConnector(unified resource.ResourceClient, gr schema.GroupResource) LatestConnector {
return &latestREST{
unified: unified,
gr: gr,
opts: opts,
scheme: scheme,
}
}
type latestREST struct {
unified resource.ResourceClient
gr schema.GroupResource
opts generic.RESTOptions
scheme *runtime.Scheme
}
func (l *latestREST) New() runtime.Object {

View File

@ -17,7 +17,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic"
)
func TestLatest(t *testing.T) {
@ -31,7 +30,6 @@ func TestLatest(t *testing.T) {
r := &latestREST{
unified: mockClient,
gr: gr,
opts: generic.RESTOptions{},
}
t.Run("no namespace in context", func(t *testing.T) {

View File

@ -6,11 +6,13 @@ import (
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/stretchr/testify/require"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard"
"github.com/grafana/grafana/pkg/services/provisioning"
)
func TestScanRow(t *testing.T) {
@ -50,8 +52,10 @@ func TestScanRow(t *testing.T) {
require.NotNil(t, row)
require.Equal(t, "Test Dashboard", row.Dash.Name)
require.Equal(t, version, row.RV) // rv should be the dashboard version
require.Equal(t, v0alpha1.Unstructured{
Object: map[string]interface{}{"key": "value"},
require.Equal(t, dashboardinternal.DashboardSpec{
Unstructured: common.Unstructured{
Object: map[string]interface{}{"key": "value"},
},
}, row.Dash.Spec)
require.Equal(t, "default", row.Dash.Namespace)
require.Equal(t, &continueToken{orgId: int64(1), id: id}, row.token)

View File

@ -4,77 +4,135 @@ import (
"context"
"errors"
"fmt"
"maps"
"path"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard"
dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
dashboardv1alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v1alpha1"
dashboardv2alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/prometheus/client_golang/prometheus"
)
var (
_ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
_ builder.OpenAPIPostProcessor = (*DashboardsAPIBuilder)(nil)
_ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
_ builder.APIGroupVersionsProvider = (*DashboardsAPIBuilder)(nil)
_ builder.OpenAPIPostProcessor = (*DashboardsAPIBuilder)(nil)
_ builder.APIGroupRouteProvider = (*DashboardsAPIBuilder)(nil)
)
// This is used just so wire has something unique to return
type DashboardsAPIBuilder struct {
ProvisioningDashboardService dashboards.DashboardProvisioningService
dashboardService dashboards.DashboardService
features featuremgmt.FeatureToggles
accessControl accesscontrol.AccessControl
legacy *DashboardStorage
unified resource.ResourceClient
dashboardProvisioningService dashboards.DashboardProvisioningService
scheme *runtime.Scheme
search *SearchHandler
log log.Logger
reg prometheus.Registerer
}
func RegisterAPIService(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
dashboardService dashboards.DashboardService,
provisioningDashboardService dashboards.DashboardProvisioningService,
accessControl accesscontrol.AccessControl,
provisioning provisioning.ProvisioningService,
dashStore dashboards.Store,
reg prometheus.Registerer,
sql db.DB,
tracing *tracing.TracingService,
unified resource.ResourceClient,
dual dualwrite.Service,
sorter sort.Service,
) *DashboardsAPIBuilder {
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
dbp := legacysql.NewDatabaseProvider(sql)
namespacer := request.GetNamespaceMapper(cfg)
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
builder := &DashboardsAPIBuilder{
ProvisioningDashboardService: provisioningDashboardService,
log: log.New("grafana-apiserver.dashboards"),
dashboardService: dashboardService,
features: features,
accessControl: accessControl,
unified: unified,
dashboardProvisioningService: provisioningDashboardService,
search: NewSearchHandler(tracing, dual, legacyDashboardSearcher, unified, features),
legacy: &DashboardStorage{
Resource: dashboardinternal.DashboardResourceInfo,
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
TableConverter: dashboardinternal.DashboardResourceInfo.TableConverter(),
Features: features,
},
reg: reg,
}
apiregistration.RegisterAPI(builder)
return builder
}
func (b *DashboardsAPIBuilder) GetGroupVersion() schema.GroupVersion {
return dashboardinternal.DashboardResourceInfo.GroupVersion()
}
func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return nil // no authorizer
}
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
if err := dashboardinternal.AddToScheme(scheme); err != nil {
return err
}
return scheme.SetVersionPriority(
func (b *DashboardsAPIBuilder) GetGroupVersions() []schema.GroupVersion {
return []schema.GroupVersion{
dashboardv0alpha1.DashboardResourceInfo.GroupVersion(),
dashboardv1alpha1.DashboardResourceInfo.GroupVersion(),
dashboardv2alpha1.DashboardResourceInfo.GroupVersion(),
)
}
}
func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
return nil
}
func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return nil
}
func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
return oas, nil
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
b.scheme = scheme
if err := dashboardinternal.AddToScheme(scheme); err != nil {
return err
}
if err := dashboardv0alpha1.AddToScheme(scheme); err != nil {
return err
}
if err := dashboardv1alpha1.AddToScheme(scheme); err != nil {
return err
}
if err := dashboardv2alpha1.AddToScheme(scheme); err != nil {
return err
}
return scheme.SetVersionPriority(b.GetGroupVersions()...)
}
// Validate will prevent deletion of provisioned dashboards, unless the grace period is set to 0, indicating a force deletion
@ -93,7 +151,7 @@ func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attribu
return fmt.Errorf("%v: %w", "failed to parse namespace", err)
}
provisioningData, err := b.ProvisioningDashboardService.GetProvisionedDashboardDataByDashboardUID(ctx, nsInfo.OrgID, a.GetName())
provisioningData, err := b.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardUID(ctx, nsInfo.OrgID, a.GetName())
if err != nil {
if errors.Is(err, dashboards.ErrProvisionedDashboardNotFound) {
return nil
@ -110,3 +168,178 @@ func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attribu
return nil
}
// Mutate removes any internal ID set in the spec & adds it as a label
func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
op := a.GetOperation()
if op != admission.Create && op != admission.Update {
return nil
}
obj := a.GetObject()
dash, ok := obj.(*dashboardinternal.Dashboard)
if !ok {
return fmt.Errorf("mutation error: expected *dashboardinternal.Dashboard, got %T", obj)
}
if id, ok := dash.Spec.Object["id"].(float64); ok {
delete(dash.Spec.Object, "id")
if id != 0 {
meta, err := utils.MetaAccessor(obj)
if err != nil {
return err
}
meta.SetDeprecatedInternalID(int64(id)) // nolint:staticcheck
}
}
return nil
}
func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
internalDashResourceInfo := dashboardinternal.DashboardResourceInfo
legacyStore, err := b.legacy.NewStore(opts.Scheme, opts.OptsGetter, b.reg)
if err != nil {
return err
}
storageOpts := apistore.StorageOptions{
RequireDeprecatedInternalID: true,
}
// Split dashboards when they are large
var largeObjects apistore.LargeObjectSupport
if b.legacy.Features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageBigObjectsSupport) {
largeObjects = NewDashboardLargeObjectSupport(opts.Scheme)
storageOpts.LargeObjectSupport = largeObjects
}
opts.StorageOptions(internalDashResourceInfo.GroupResource(), storageOpts)
// v0alpha1
storage, err := b.storageForVersion(opts, legacyStore, largeObjects, func() runtime.Object {
return &dashboardv0alpha1.DashboardWithAccessInfo{}
})
if err != nil {
return err
}
apiGroupInfo.VersionedResourcesStorageMap[dashboardv0alpha1.VERSION] = storage
// v1alpha1
storage, err = b.storageForVersion(opts, legacyStore, largeObjects, func() runtime.Object {
return &dashboardv1alpha1.DashboardWithAccessInfo{}
})
if err != nil {
return err
}
apiGroupInfo.VersionedResourcesStorageMap[dashboardv1alpha1.VERSION] = storage
// v2alpha1
storage, err = b.storageForVersion(opts, legacyStore, largeObjects, func() runtime.Object {
return &dashboardv2alpha1.DashboardWithAccessInfo{}
})
if err != nil {
return err
}
apiGroupInfo.VersionedResourcesStorageMap[dashboardv2alpha1.VERSION] = storage
return nil
}
func (b *DashboardsAPIBuilder) storageForVersion(
opts builder.APIGroupOptions,
legacyStore grafanarest.LegacyStorage,
largeObjects apistore.LargeObjectSupport,
newDTOFunc func() runtime.Object,
) (map[string]rest.Storage, error) {
var (
err error
scheme = opts.Scheme
dualWriteBuilder = opts.DualWriteBuilder
internalDashResourceInfo = dashboardinternal.DashboardResourceInfo
libraryPanelResourceInfo = dashboardinternal.LibraryPanelResourceInfo
)
storage := map[string]rest.Storage{}
storage[internalDashResourceInfo.StoragePath()] = legacyStore
// Dual writes if a RESTOptionsGetter is provided
if dualWriteBuilder != nil {
store, err := grafanaregistry.NewRegistryStore(scheme, internalDashResourceInfo, opts.OptsGetter)
if err != nil {
return nil, err
}
storage[internalDashResourceInfo.StoragePath()], err = dualWriteBuilder(internalDashResourceInfo.GroupResource(), legacyStore, store)
if err != nil {
return nil, err
}
}
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesRestore) {
storage[internalDashResourceInfo.StoragePath("restore")] = NewRestoreConnector(
b.unified,
internalDashResourceInfo.GroupResource(),
)
storage[internalDashResourceInfo.StoragePath("latest")] = NewLatestConnector(
b.unified,
internalDashResourceInfo.GroupResource(),
)
}
// Register the DTO endpoint that will consolidate all dashboard bits
storage[internalDashResourceInfo.StoragePath("dto")], err = NewDTOConnector(
storage[internalDashResourceInfo.StoragePath()],
largeObjects,
b.legacy.Access,
b.unified,
b.accessControl,
scheme,
newDTOFunc,
)
if err != nil {
return nil, err
}
// Expose read only library panels
storage[libraryPanelResourceInfo.StoragePath()] = &LibraryPanelStore{
Access: b.legacy.Access,
ResourceInfo: dashboardinternal.LibraryPanelResourceInfo,
}
return storage, nil
}
func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
defs := dashboardv0alpha1.GetOpenAPIDefinitions(ref)
maps.Copy(defs, dashboardv1alpha1.GetOpenAPIDefinitions(ref))
maps.Copy(defs, dashboardv2alpha1.GetOpenAPIDefinitions(ref))
return defs
}
}
func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
// The plugin description
oas.Info.Description = "Grafana dashboards as resources"
for _, gv := range b.GetGroupVersions() {
version := gv.Version
// Hide cluster-scoped resources
root := path.Join("/apis/", dashboardinternal.GROUP, version)
delete(oas.Paths.Paths, path.Join(root, "dashboards"))
delete(oas.Paths.Paths, path.Join(root, "watch", "dashboards"))
if version == dashboardv0alpha1.VERSION {
sub := oas.Paths.Paths[path.Join(root, "search", "{name}")]
oas.Paths.Paths[path.Join(root, "search")] = sub
delete(oas.Paths.Paths, path.Join(root, "search", "{name}"))
}
}
return oas, nil
}
func (b *DashboardsAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} })
return b.search.GetAPIRoutes(defs)
}

View File

@ -128,7 +128,7 @@ func TestDashboardAPIBuilder_Validate(t *testing.T) {
fakeService := &dashboards.FakeDashboardProvisioning{}
fakeService.On("GetProvisionedDashboardDataByDashboardUID", mock.Anything, mock.Anything, mock.Anything).Return(tt.dashboardResponse, tt.dashboardErrorResponse).Once()
b := &DashboardsAPIBuilder{
ProvisioningDashboardService: fakeService,
dashboardProvisioningService: fakeService,
}
err := b.Validate(context.Background(), admission.NewAttributesRecord(
tt.inputObj,

View File

@ -11,7 +11,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storage"
@ -25,18 +24,16 @@ type RestoreConnector interface {
rest.StorageMetadata
}
func NewRestoreConnector(unified resource.ResourceClient, gr schema.GroupResource, opts generic.RESTOptions) RestoreConnector {
func NewRestoreConnector(unified resource.ResourceClient, gr schema.GroupResource) RestoreConnector {
return &restoreREST{
unified: unified,
gr: gr,
opts: opts,
}
}
type restoreREST struct {
unified resource.ResourceClient
gr schema.GroupResource
opts generic.RESTOptions
}
func (r *restoreREST) New() runtime.Object {

View File

@ -17,7 +17,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic"
)
type mockResourceClient struct {
@ -58,7 +57,6 @@ func TestRestore(t *testing.T) {
r := &restoreREST{
unified: mockClient,
gr: gr,
opts: generic.RESTOptions{},
}
t.Run("no namespace in context", func(t *testing.T) {

View File

@ -1,256 +0,0 @@
package v0alpha1
import (
"context"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard"
dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
var (
_ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
_ builder.OpenAPIPostProcessor = (*DashboardsAPIBuilder)(nil)
)
// This is used just so wire has something unique to return
type DashboardsAPIBuilder struct {
dashboard.DashboardsAPIBuilder
dashboardService dashboards.DashboardService
features featuremgmt.FeatureToggles
accessControl accesscontrol.AccessControl
legacy *dashboard.DashboardStorage
search *dashboard.SearchHandler
unified resource.ResourceClient
log log.Logger
reg prometheus.Registerer
}
func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
dashboardService dashboards.DashboardService,
provisioningDashboardService dashboards.DashboardProvisioningService,
accessControl accesscontrol.AccessControl,
provisioning provisioning.ProvisioningService,
dashStore dashboards.Store,
reg prometheus.Registerer,
sql db.DB,
tracing *tracing.TracingService,
unified resource.ResourceClient,
dual dualwrite.Service,
sorter sort.Service,
) *DashboardsAPIBuilder {
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
dbp := legacysql.NewDatabaseProvider(sql)
namespacer := request.GetNamespaceMapper(cfg)
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
builder := &DashboardsAPIBuilder{
log: log.New("grafana-apiserver.dashboards.v0alpha1"),
DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
ProvisioningDashboardService: provisioningDashboardService,
},
dashboardService: dashboardService,
features: features,
accessControl: accessControl,
unified: unified,
search: dashboard.NewSearchHandler(tracing, dual, legacyDashboardSearcher, unified, features),
legacy: &dashboard.DashboardStorage{
Resource: dashboardv0alpha1.DashboardResourceInfo,
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
TableConverter: dashboardv0alpha1.DashboardResourceInfo.TableConverter(),
Features: features,
},
reg: reg,
}
apiregistration.RegisterAPI(builder)
return builder
}
func (b *DashboardsAPIBuilder) GetGroupVersion() schema.GroupVersion {
return dashboardv0alpha1.DashboardResourceInfo.GroupVersion()
}
func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return dashboard.GetAuthorizer(b.dashboardService, b.log)
}
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return dashboardv0alpha1.AddToScheme(scheme)
}
func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
scheme := opts.Scheme
optsGetter := opts.OptsGetter
dualWriteBuilder := opts.DualWriteBuilder
dash := b.legacy.Resource
legacyStore, err := b.legacy.NewStore(scheme, optsGetter, b.reg)
if err != nil {
return err
}
defaultOpts, err := optsGetter.GetRESTOptions(b.legacy.Resource.GroupResource(), &dashboardinternal.Dashboard{})
if err != nil {
return err
}
storageOpts := apistore.StorageOptions{
RequireDeprecatedInternalID: true,
InternalConversion: (func(b []byte, desiredObj runtime.Object) (runtime.Object, error) {
internal := &dashboardinternal.Dashboard{}
obj, _, err := defaultOpts.StorageConfig.Config.Codec.Decode(b, nil, internal)
if err != nil {
return nil, err
}
err = scheme.Convert(obj, desiredObj, nil)
return desiredObj, err
}),
}
// Split dashboards when they are large
var largeObjects apistore.LargeObjectSupport
if b.legacy.Features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageBigObjectsSupport) {
largeObjects = dashboard.NewDashboardLargeObjectSupport(scheme)
storageOpts.LargeObjectSupport = largeObjects
}
opts.StorageOptions(dash.GroupResource(), storageOpts)
storage := map[string]rest.Storage{}
storage[dash.StoragePath()] = legacyStore
// Dual writes if a RESTOptionsGetter is provided
if dualWriteBuilder != nil {
store, err := grafanaregistry.NewRegistryStore(scheme, dash, optsGetter)
if err != nil {
return err
}
storage[dash.StoragePath()], err = dualWriteBuilder(dash.GroupResource(), legacyStore, store)
if err != nil {
return err
}
}
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesRestore) {
storage[dash.StoragePath("restore")] = dashboard.NewRestoreConnector(
b.unified,
dashboardv0alpha1.DashboardResourceInfo.GroupResource(),
defaultOpts,
)
storage[dash.StoragePath("latest")] = dashboard.NewLatestConnector(
b.unified,
dashboardv0alpha1.DashboardResourceInfo.GroupResource(),
defaultOpts,
scheme,
)
}
// Register the DTO endpoint that will consolidate all dashboard bits
storage[dash.StoragePath("dto")], err = dashboard.NewDTOConnector(
storage[dash.StoragePath()],
largeObjects,
b.legacy.Access,
b.unified,
b.accessControl,
scheme,
func() runtime.Object { return &dashboardv0alpha1.DashboardWithAccessInfo{} },
)
if err != nil {
return err
}
// Expose read only library panels
storage[dashboardv0alpha1.LibraryPanelResourceInfo.StoragePath()] = &dashboard.LibraryPanelStore{
Access: b.legacy.Access,
ResourceInfo: dashboardv0alpha1.LibraryPanelResourceInfo,
}
apiGroupInfo.VersionedResourcesStorageMap[dashboardv0alpha1.VERSION] = storage
return nil
}
func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return dashboardv0alpha1.GetOpenAPIDefinitions
}
func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
// The plugin description
oas.Info.Description = "Grafana dashboards as resources"
// The root api URL
root := "/apis/" + b.GetGroupVersion().String() + "/"
// Hide the ability to list or watch across all tenants
delete(oas.Paths.Paths, root+dashboardv0alpha1.DashboardResourceInfo.GroupResource().Resource)
delete(oas.Paths.Paths, root+"watch/"+dashboardv0alpha1.DashboardResourceInfo.GroupResource().Resource)
// Resolve the empty name
sub := oas.Paths.Paths[root+"search/{name}"]
oas.Paths.Paths[root+"search"] = sub
delete(oas.Paths.Paths, root+"search/{name}")
return oas, nil
}
func (b *DashboardsAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} })
return b.search.GetAPIRoutes(defs)
}
// Mutate removes any internal ID set in the spec & adds it as a label
func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
op := a.GetOperation()
if op == admission.Create || op == admission.Update {
obj := a.GetObject()
dash, ok := obj.(*dashboardv0alpha1.Dashboard)
if !ok {
return fmt.Errorf("expected v0alpha1 dashboard")
}
if id, ok := dash.Spec.Object["id"].(float64); ok {
delete(dash.Spec.Object, "id")
if id != 0 {
meta, err := utils.MetaAccessor(obj)
if err != nil {
return err
}
meta.SetDeprecatedInternalID(int64(id)) // nolint:staticcheck
}
}
}
return nil
}

View File

@ -1,162 +0,0 @@
package v0alpha1
import (
"context"
"testing"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission"
)
func TestDashboardAPIBuilder_Mutate(t *testing.T) {
tests := []struct {
name string
verb string
input *v0alpha1.Dashboard
expected *v0alpha1.Dashboard
}{
{
name: "should remove id and add as label in create",
verb: "CREATE",
input: &v0alpha1.Dashboard{
Spec: common.Unstructured{
Object: map[string]interface{}{
"id": float64(1),
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
expected: &v0alpha1.Dashboard{
Spec: common.Unstructured{
Object: map[string]interface{}{},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Labels: map[string]string{"grafana.app/deprecatedInternalID": "1"},
},
},
},
{
name: "should remove id and add as label in update",
verb: "UPDATE",
input: &v0alpha1.Dashboard{
Spec: common.Unstructured{
Object: map[string]interface{}{
"id": float64(1),
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
expected: &v0alpha1.Dashboard{
Spec: common.Unstructured{
Object: map[string]interface{}{},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Labels: map[string]string{"grafana.app/deprecatedInternalID": "1"},
},
},
},
{
name: "should only remove id ",
verb: "UPDATE",
input: &v0alpha1.Dashboard{
Spec: common.Unstructured{
Object: map[string]interface{}{
"id": float64(1),
"testing": "this",
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
expected: &v0alpha1.Dashboard{
Spec: common.Unstructured{
Object: map[string]interface{}{
"testing": "this",
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Labels: map[string]string{"grafana.app/deprecatedInternalID": "1"},
},
},
},
{
name: "should not set label if id is 0",
verb: "CREATE",
input: &v0alpha1.Dashboard{
Spec: common.Unstructured{
Object: map[string]interface{}{
"id": float64(0),
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
expected: &v0alpha1.Dashboard{
Spec: common.Unstructured{
Object: map[string]interface{}{},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
},
}
b := &DashboardsAPIBuilder{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := b.Mutate(context.Background(), admission.NewAttributesRecord(
tt.input,
nil,
v0alpha1.DashboardResourceInfo.GroupVersionKind(),
"stacks-123",
tt.input.Name,
v0alpha1.DashboardResourceInfo.GroupVersionResource(),
"",
admission.Operation(tt.verb),
nil,
true,
&user.SignedInUser{},
), nil)
require.NoError(t, err)
require.Equal(t, tt.expected, tt.input)
})
}
}

View File

@ -1,239 +0,0 @@
package v1alpha1
import (
"context"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard"
dashboardv1alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v1alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
var (
_ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
_ builder.OpenAPIPostProcessor = (*DashboardsAPIBuilder)(nil)
)
// This is used just so wire has something unique to return
type DashboardsAPIBuilder struct {
dashboard.DashboardsAPIBuilder
dashboardService dashboards.DashboardService
features featuremgmt.FeatureToggles
accessControl accesscontrol.AccessControl
legacy *dashboard.DashboardStorage
unified resource.ResourceClient
log log.Logger
reg prometheus.Registerer
}
func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
dashboardService dashboards.DashboardService,
provisioningDashboardService dashboards.DashboardProvisioningService,
accessControl accesscontrol.AccessControl,
provisioning provisioning.ProvisioningService,
dashStore dashboards.Store,
reg prometheus.Registerer,
sql db.DB,
tracing *tracing.TracingService,
unified resource.ResourceClient,
sorter sort.Service,
) *DashboardsAPIBuilder {
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
dbp := legacysql.NewDatabaseProvider(sql)
namespacer := request.GetNamespaceMapper(cfg)
builder := &DashboardsAPIBuilder{
log: log.New("grafana-apiserver.dashboards.v1alpha1"),
DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
ProvisioningDashboardService: provisioningDashboardService,
},
dashboardService: dashboardService,
features: features,
accessControl: accessControl,
unified: unified,
legacy: &dashboard.DashboardStorage{
Resource: dashboardv1alpha1.DashboardResourceInfo,
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
TableConverter: dashboardv1alpha1.DashboardResourceInfo.TableConverter(),
Features: features,
},
reg: reg,
}
apiregistration.RegisterAPI(builder)
return builder
}
func (b *DashboardsAPIBuilder) GetGroupVersion() schema.GroupVersion {
return dashboardv1alpha1.DashboardResourceInfo.GroupVersion()
}
func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return dashboard.GetAuthorizer(b.dashboardService, b.log)
}
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return dashboardv1alpha1.AddToScheme(scheme)
}
func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
scheme := opts.Scheme
optsGetter := opts.OptsGetter
dualWriteBuilder := opts.DualWriteBuilder
dash := b.legacy.Resource
legacyStore, err := b.legacy.NewStore(scheme, optsGetter, b.reg)
if err != nil {
return err
}
defaultOpts, err := optsGetter.GetRESTOptions(b.legacy.Resource.GroupResource(), &dashboardinternal.Dashboard{})
if err != nil {
return err
}
storageOpts := apistore.StorageOptions{
RequireDeprecatedInternalID: true,
InternalConversion: (func(b []byte, desiredObj runtime.Object) (runtime.Object, error) {
internal := &dashboardinternal.Dashboard{}
obj, _, err := defaultOpts.StorageConfig.Config.Codec.Decode(b, nil, internal)
if err != nil {
return nil, err
}
err = scheme.Convert(obj, desiredObj, nil)
return desiredObj, err
}),
}
// Split dashboards when they are large
var largeObjects apistore.LargeObjectSupport
if b.legacy.Features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageBigObjectsSupport) {
largeObjects = dashboard.NewDashboardLargeObjectSupport(scheme)
storageOpts.LargeObjectSupport = largeObjects
}
opts.StorageOptions(dash.GroupResource(), storageOpts)
storage := map[string]rest.Storage{}
storage[dash.StoragePath()] = legacyStore
// Dual writes if a RESTOptionsGetter is provided
if dualWriteBuilder != nil {
store, err := grafanaregistry.NewRegistryStore(scheme, dash, optsGetter)
if err != nil {
return err
}
storage[dash.StoragePath()], err = dualWriteBuilder(dash.GroupResource(), legacyStore, store)
if err != nil {
return err
}
}
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesRestore) {
storage[dash.StoragePath("restore")] = dashboard.NewRestoreConnector(
b.unified,
dashboardv1alpha1.DashboardResourceInfo.GroupResource(),
defaultOpts,
)
storage[dash.StoragePath("latest")] = dashboard.NewLatestConnector(
b.unified,
dashboardv1alpha1.DashboardResourceInfo.GroupResource(),
defaultOpts,
scheme,
)
}
// Register the DTO endpoint that will consolidate all dashboard bits
storage[dash.StoragePath("dto")], err = dashboard.NewDTOConnector(
storage[dash.StoragePath()],
largeObjects,
b.legacy.Access,
b.unified,
b.accessControl,
scheme,
func() runtime.Object { return &dashboardv1alpha1.DashboardWithAccessInfo{} },
)
if err != nil {
return err
}
// Expose read only library panels
storage[dashboardv1alpha1.LibraryPanelResourceInfo.StoragePath()] = &dashboard.LibraryPanelStore{
Access: b.legacy.Access,
ResourceInfo: dashboardv1alpha1.LibraryPanelResourceInfo,
}
apiGroupInfo.VersionedResourcesStorageMap[dashboardv1alpha1.VERSION] = storage
return nil
}
func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return dashboardv1alpha1.GetOpenAPIDefinitions
}
func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
// The plugin description
oas.Info.Description = "Grafana dashboards as resources"
// The root api URL
root := "/apis/" + b.GetGroupVersion().String() + "/"
// Hide the ability to list or watch across all tenants
delete(oas.Paths.Paths, root+dashboardv1alpha1.DashboardResourceInfo.GroupResource().Resource)
delete(oas.Paths.Paths, root+"watch/"+dashboardv1alpha1.DashboardResourceInfo.GroupResource().Resource)
return oas, nil
}
// Mutate removes any internal ID set in the spec & adds it as a label
func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
op := a.GetOperation()
if op == admission.Create || op == admission.Update {
obj := a.GetObject()
dash, ok := obj.(*dashboardv1alpha1.Dashboard)
if !ok {
return fmt.Errorf("expected v1alpha1 dashboard")
}
if id, ok := dash.Spec.Object["id"].(float64); ok {
delete(dash.Spec.Object, "id")
if id != 0 {
meta, err := utils.MetaAccessor(obj)
if err != nil {
return err
}
meta.SetDeprecatedInternalID(int64(id)) // nolint:staticcheck
}
}
}
return nil
}

View File

@ -1,186 +0,0 @@
package v1alpha1
import (
"context"
"testing"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apis/dashboard/v1alpha1"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission"
)
func TestDashboardAPIBuilder_Mutate(t *testing.T) {
tests := []struct {
name string
verb string
input *v1alpha1.Dashboard
expected *v1alpha1.Dashboard
}{
{
name: "should remove id and add as label in create",
verb: "CREATE",
input: &v1alpha1.Dashboard{
Spec: v1alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{
"id": float64(1),
},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
expected: &v1alpha1.Dashboard{
Spec: v1alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Labels: map[string]string{"grafana.app/deprecatedInternalID": "1"},
},
},
},
{
name: "should remove id and add as label in update",
verb: "UPDATE",
input: &v1alpha1.Dashboard{
Spec: v1alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{
"id": float64(1),
},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
expected: &v1alpha1.Dashboard{
Spec: v1alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Labels: map[string]string{"grafana.app/deprecatedInternalID": "1"},
},
},
},
{
name: "should only remove id ",
verb: "UPDATE",
input: &v1alpha1.Dashboard{
Spec: v1alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{
"id": float64(1),
"testing": "this",
},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
expected: &v1alpha1.Dashboard{
Spec: v1alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{
"testing": "this",
},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Labels: map[string]string{"grafana.app/deprecatedInternalID": "1"},
},
},
},
{
name: "should not set label if id is 0",
verb: "CREATE",
input: &v1alpha1.Dashboard{
Spec: v1alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{
"id": float64(0),
},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
expected: &v1alpha1.Dashboard{
Spec: v1alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
},
}
b := &DashboardsAPIBuilder{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := b.Mutate(context.Background(), admission.NewAttributesRecord(
tt.input,
nil,
v1alpha1.DashboardResourceInfo.GroupVersionKind(),
"stacks-123",
tt.input.Name,
v1alpha1.DashboardResourceInfo.GroupVersionResource(),
"",
admission.Operation(tt.verb),
nil,
true,
&user.SignedInUser{},
), nil)
require.NoError(t, err)
require.Equal(t, tt.expected, tt.input)
})
}
}

View File

@ -1,239 +0,0 @@
package v2alpha1
import (
"context"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard"
dashboardv2alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
var (
_ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
_ builder.OpenAPIPostProcessor = (*DashboardsAPIBuilder)(nil)
)
// This is used just so wire has something unique to return
type DashboardsAPIBuilder struct {
dashboard.DashboardsAPIBuilder
dashboardService dashboards.DashboardService
features featuremgmt.FeatureToggles
accessControl accesscontrol.AccessControl
legacy *dashboard.DashboardStorage
unified resource.ResourceClient
log log.Logger
reg prometheus.Registerer
}
func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
dashboardService dashboards.DashboardService,
provisioningDashboardService dashboards.DashboardProvisioningService,
accessControl accesscontrol.AccessControl,
provisioning provisioning.ProvisioningService,
dashStore dashboards.Store,
reg prometheus.Registerer,
sql db.DB,
tracing *tracing.TracingService,
unified resource.ResourceClient,
sorter sort.Service,
) *DashboardsAPIBuilder {
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
dbp := legacysql.NewDatabaseProvider(sql)
namespacer := request.GetNamespaceMapper(cfg)
builder := &DashboardsAPIBuilder{
log: log.New("grafana-apiserver.dashboards.v2alpha1"),
DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
ProvisioningDashboardService: provisioningDashboardService,
},
dashboardService: dashboardService,
features: features,
accessControl: accessControl,
unified: unified,
legacy: &dashboard.DashboardStorage{
Resource: dashboardv2alpha1.DashboardResourceInfo,
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
TableConverter: dashboardv2alpha1.DashboardResourceInfo.TableConverter(),
Features: features,
},
reg: reg,
}
apiregistration.RegisterAPI(builder)
return builder
}
func (b *DashboardsAPIBuilder) GetGroupVersion() schema.GroupVersion {
return dashboardv2alpha1.DashboardResourceInfo.GroupVersion()
}
func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return dashboard.GetAuthorizer(b.dashboardService, b.log)
}
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return dashboardv2alpha1.AddToScheme(scheme)
}
func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
scheme := opts.Scheme
optsGetter := opts.OptsGetter
dualWriteBuilder := opts.DualWriteBuilder
dash := b.legacy.Resource
legacyStore, err := b.legacy.NewStore(scheme, optsGetter, b.reg)
if err != nil {
return err
}
defaultOpts, err := optsGetter.GetRESTOptions(b.legacy.Resource.GroupResource(), &dashboardinternal.Dashboard{})
if err != nil {
return err
}
storageOpts := apistore.StorageOptions{
RequireDeprecatedInternalID: true,
InternalConversion: (func(b []byte, desiredObj runtime.Object) (runtime.Object, error) {
internal := &dashboardinternal.Dashboard{}
obj, _, err := defaultOpts.StorageConfig.Config.Codec.Decode(b, nil, internal)
if err != nil {
return nil, err
}
err = scheme.Convert(obj, desiredObj, nil)
return desiredObj, err
}),
}
// Split dashboards when they are large
var largeObjects apistore.LargeObjectSupport
if b.legacy.Features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageBigObjectsSupport) {
largeObjects = dashboard.NewDashboardLargeObjectSupport(scheme)
storageOpts.LargeObjectSupport = largeObjects
}
opts.StorageOptions(dash.GroupResource(), storageOpts)
storage := map[string]rest.Storage{}
storage[dash.StoragePath()] = legacyStore
// Dual writes if a RESTOptionsGetter is provided
if dualWriteBuilder != nil {
store, err := grafanaregistry.NewRegistryStore(scheme, dash, optsGetter)
if err != nil {
return err
}
storage[dash.StoragePath()], err = dualWriteBuilder(dash.GroupResource(), legacyStore, store)
if err != nil {
return err
}
}
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesRestore) {
storage[dash.StoragePath("restore")] = dashboard.NewRestoreConnector(
b.unified,
dashboardv2alpha1.DashboardResourceInfo.GroupResource(),
defaultOpts,
)
storage[dash.StoragePath("latest")] = dashboard.NewLatestConnector(
b.unified,
dashboardv2alpha1.DashboardResourceInfo.GroupResource(),
defaultOpts,
scheme,
)
}
// Register the DTO endpoint that will consolidate all dashboard bits
storage[dash.StoragePath("dto")], err = dashboard.NewDTOConnector(
storage[dash.StoragePath()],
largeObjects,
b.legacy.Access,
b.unified,
b.accessControl,
scheme,
func() runtime.Object { return &dashboardv2alpha1.DashboardWithAccessInfo{} },
)
if err != nil {
return err
}
// Expose read only library panels
storage[dashboardv2alpha1.LibraryPanelResourceInfo.StoragePath()] = &dashboard.LibraryPanelStore{
Access: b.legacy.Access,
ResourceInfo: dashboardv2alpha1.LibraryPanelResourceInfo,
}
apiGroupInfo.VersionedResourcesStorageMap[dashboardv2alpha1.VERSION] = storage
return nil
}
func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return dashboardv2alpha1.GetOpenAPIDefinitions
}
func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
// The plugin description
oas.Info.Description = "Grafana dashboards as resources"
// The root api URL
root := "/apis/" + b.GetGroupVersion().String() + "/"
// Hide the ability to list or watch across all tenants
delete(oas.Paths.Paths, root+dashboardv2alpha1.DashboardResourceInfo.GroupResource().Resource)
return oas, nil
}
// Mutate removes any internal ID set in the spec & adds it as a label
func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
op := a.GetOperation()
if op == admission.Create || op == admission.Update {
obj := a.GetObject()
dash, ok := obj.(*dashboardv2alpha1.Dashboard)
if !ok {
return fmt.Errorf("expected v2alpha1 dashboard")
}
if id, ok := dash.Spec.Object["id"].(float64); ok {
delete(dash.Spec.Object, "id")
if id != 0 {
meta, err := utils.MetaAccessor(obj)
if err != nil {
return err
}
meta.SetDeprecatedInternalID(int64(id)) // nolint:staticcheck
}
}
}
return nil
}

View File

@ -1,186 +0,0 @@
package v2alpha1
import (
"context"
"testing"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission"
)
func TestDashboardAPIBuilder_Mutate(t *testing.T) {
tests := []struct {
name string
verb string
input *v2alpha1.Dashboard
expected *v2alpha1.Dashboard
}{
{
name: "should remove id and add as label in create",
verb: "CREATE",
input: &v2alpha1.Dashboard{
Spec: v2alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{
"id": float64(1),
},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
expected: &v2alpha1.Dashboard{
Spec: v2alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Labels: map[string]string{"grafana.app/deprecatedInternalID": "1"},
},
},
},
{
name: "should remove id and add as label in update",
verb: "UPDATE",
input: &v2alpha1.Dashboard{
Spec: v2alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{
"id": float64(1),
},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
expected: &v2alpha1.Dashboard{
Spec: v2alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Labels: map[string]string{"grafana.app/deprecatedInternalID": "1"},
},
},
},
{
name: "should only remove id ",
verb: "UPDATE",
input: &v2alpha1.Dashboard{
Spec: v2alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{
"id": float64(1),
"testing": "this",
},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
expected: &v2alpha1.Dashboard{
Spec: v2alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{
"testing": "this",
},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Labels: map[string]string{"grafana.app/deprecatedInternalID": "1"},
},
},
},
{
name: "should not set label if id is 0",
verb: "CREATE",
input: &v2alpha1.Dashboard{
Spec: v2alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{
"id": float64(0),
},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
expected: &v2alpha1.Dashboard{
Spec: v2alpha1.DashboardSpec{
Title: "test",
Unstructured: common.Unstructured{
Object: map[string]interface{}{},
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Dashboard",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
},
}
b := &DashboardsAPIBuilder{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := b.Mutate(context.Background(), admission.NewAttributesRecord(
tt.input,
nil,
v2alpha1.DashboardResourceInfo.GroupVersionKind(),
"stacks-123",
tt.input.Name,
v2alpha1.DashboardResourceInfo.GroupVersionResource(),
"",
admission.Operation(tt.verb),
nil,
true,
&user.SignedInUser{},
), nil)
require.NoError(t, err)
require.Equal(t, tt.expected, tt.input)
})
}
}

View File

@ -5,9 +5,6 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications"
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
dashboardv0alpha1 "github.com/grafana/grafana/pkg/registry/apis/dashboard/v0alpha1"
dashboardv1alpha1 "github.com/grafana/grafana/pkg/registry/apis/dashboard/v1alpha1"
dashboardv2alpha1 "github.com/grafana/grafana/pkg/registry/apis/dashboard/v2alpha1"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
@ -30,9 +27,6 @@ var WireSet = wire.NewSet(
// Each must be added here *and* in the ServiceSink above
dashboardinternal.RegisterAPIService,
dashboardv0alpha1.RegisterAPIService,
dashboardv1alpha1.RegisterAPIService,
dashboardv2alpha1.RegisterAPIService,
dashboardsnapshot.RegisterAPIService,
featuretoggle.RegisterAPIService,
datasource.RegisterAPIService,

View File

@ -6,6 +6,7 @@
package apistore
import (
"bytes"
"context"
"errors"
"fmt"
@ -47,7 +48,6 @@ var _ storage.Interface = (*Storage)(nil)
// Optional settings that apply to a single resource
type StorageOptions struct {
LargeObjectSupport LargeObjectSupport
InternalConversion func([]byte, runtime.Object) (runtime.Object, error)
RequireDeprecatedInternalID bool
}
@ -148,9 +148,6 @@ func (s *Storage) Versioner() storage.Versioner {
}
func (s *Storage) convertToObject(data []byte, obj runtime.Object) (runtime.Object, error) {
if s.opts.InternalConversion != nil {
return s.opts.InternalConversion(data, obj)
}
obj, _, err := s.codec.Decode(data, nil, obj)
return obj, err
}
@ -180,7 +177,7 @@ func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, ou
return resource.GetError(rsp.Error)
}
if err := copyModifiedObjectToDestination(obj, out); err != nil {
if _, err := s.convertToObject(req.Value, out); err != nil {
return err
}
@ -294,7 +291,7 @@ func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOption
}
reporter := apierrors.NewClientErrorReporter(500, "WATCH", "")
decoder := newStreamDecoder(client, s.newFunc, predicate, s.codec, cancelWatch, s.opts.InternalConversion)
decoder := newStreamDecoder(client, s.newFunc, predicate, s.codec, cancelWatch)
return watch.NewStreamWatcher(decoder, reporter), nil
}
@ -538,15 +535,22 @@ func (s *Storage) GuaranteedUpdate(
}
if unchanged {
if err := copyModifiedObjectToDestination(updatedObj, destination); err != nil {
var buf bytes.Buffer
if err = s.codec.Encode(updatedObj, &buf); err != nil {
return err
}
if _, err := s.convertToObject(buf.Bytes(), destination); err != nil {
return err
}
return nil
}
rv := int64(0)
var (
value []byte
rv int64
)
if created {
value, err := s.prepareObjectForStorage(ctx, updatedObj)
value, err = s.prepareObjectForStorage(ctx, updatedObj)
if err != nil {
return err
}
@ -562,10 +566,11 @@ func (s *Storage) GuaranteedUpdate(
}
rv = rsp2.ResourceVersion
} else {
req.Value, err = s.prepareObjectForUpdate(ctx, updatedObj, existingObj)
value, err = s.prepareObjectForUpdate(ctx, updatedObj, existingObj)
if err != nil {
return err
}
req.Value = value
rsp2, err := s.store.Update(ctx, req)
if err != nil {
return resource.GetError(resource.AsErrorResult(err))
@ -576,11 +581,11 @@ func (s *Storage) GuaranteedUpdate(
rv = rsp2.ResourceVersion
}
if err := s.versioner.UpdateObject(updatedObj, uint64(rv)); err != nil {
if _, err := s.convertToObject(value, destination); err != nil {
return err
}
if err := copyModifiedObjectToDestination(updatedObj, destination); err != nil {
if err := s.versioner.UpdateObject(destination, uint64(rv)); err != nil {
return err
}
@ -634,16 +639,3 @@ func (s *Storage) validateMinimumResourceVersion(minimumResourceVersion string,
}
return nil
}
func copyModifiedObjectToDestination(updatedObj runtime.Object, destination runtime.Object) error {
u, err := conversion.EnforcePtr(updatedObj)
if err != nil {
return fmt.Errorf("unable to enforce updated object pointer: %w", err)
}
d, err := conversion.EnforcePtr(destination)
if err != nil {
return fmt.Errorf("unable to enforce destination pointer: %w", err)
}
d.Set(u)
return nil
}

View File

@ -19,33 +19,27 @@ import (
)
type streamDecoder struct {
client resource.ResourceStore_WatchClient
newFunc func() runtime.Object
predicate storage.SelectionPredicate
codec runtime.Codec
cancelWatch context.CancelFunc
done sync.WaitGroup
internalConversion func([]byte, runtime.Object) (runtime.Object, error)
client resource.ResourceStore_WatchClient
newFunc func() runtime.Object
predicate storage.SelectionPredicate
codec runtime.Codec
cancelWatch context.CancelFunc
done sync.WaitGroup
}
func newStreamDecoder(client resource.ResourceStore_WatchClient, newFunc func() runtime.Object, predicate storage.SelectionPredicate, codec runtime.Codec, cancelWatch context.CancelFunc, internalConversion func([]byte, runtime.Object) (runtime.Object, error)) *streamDecoder {
func newStreamDecoder(client resource.ResourceStore_WatchClient, newFunc func() runtime.Object, predicate storage.SelectionPredicate, codec runtime.Codec, cancelWatch context.CancelFunc) *streamDecoder {
return &streamDecoder{
client: client,
newFunc: newFunc,
predicate: predicate,
codec: codec,
cancelWatch: cancelWatch,
internalConversion: internalConversion,
client: client,
newFunc: newFunc,
predicate: predicate,
codec: codec,
cancelWatch: cancelWatch,
}
}
func (d *streamDecoder) toObject(w *resource.WatchEvent_Resource) (runtime.Object, error) {
var obj runtime.Object
var err error
if d.internalConversion != nil {
obj, err = d.internalConversion(w.Value, d.newFunc())
} else {
obj, _, err = d.codec.Decode(w.Value, nil, d.newFunc())
}
obj, _, err = d.codec.Decode(w.Value, nil, d.newFunc())
if err == nil {
accessor, err := utils.MetaAccessor(obj)
if err != nil {

View File

@ -1,34 +0,0 @@
package apistore
import (
"testing"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
func TestStreamDecoder(t *testing.T) {
t.Run("toObject should handle internal conversion", func(t *testing.T) {
called := false
internalConversion := func(data []byte, obj runtime.Object) (runtime.Object, error) {
called = true
return obj, nil
}
decoder := &streamDecoder{
newFunc: func() runtime.Object { return &unstructured.Unstructured{} },
internalConversion: internalConversion,
}
event := &resource.WatchEvent_Resource{
Value: []byte("test"),
}
obj, err := decoder.toObject(event)
require.NoError(t, err)
require.NotNil(t, obj)
require.True(t, called, "internal conversion function should have been called")
})
}