mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 20:32:24 +08:00
Dashboard: Multi-version builder (#100305)
This commit is contained in:
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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")
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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"}
|
||||
|
@ -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))
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user