Dashboard: Multi-version builder (#100305)

This commit is contained in:
Todd Treece
2025-02-21 06:50:29 -05:00
committed by GitHub
parent 7be1fd953a
commit 3992ac2ac1
31 changed files with 480 additions and 1621 deletions
-6
View File
@@ -3,9 +3,6 @@ package apiregistry
import (
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications"
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
dashboardv0alpha1 "github.com/grafana/grafana/pkg/registry/apis/dashboard/v0alpha1"
dashboardv1alpha1 "github.com/grafana/grafana/pkg/registry/apis/dashboard/v1alpha1"
dashboardv2alpha1 "github.com/grafana/grafana/pkg/registry/apis/dashboard/v2alpha1"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
@@ -22,9 +19,6 @@ type Service struct{}
// and give each builder the chance to register itself with the main server
func ProvideRegistryServiceSink(
_ *dashboardinternal.DashboardsAPIBuilder,
_ *dashboardv0alpha1.DashboardsAPIBuilder,
_ *dashboardv1alpha1.DashboardsAPIBuilder,
_ *dashboardv2alpha1.DashboardsAPIBuilder,
_ *dashboardsnapshot.SnapshotsAPIBuilder,
_ *featuretoggle.FeatureFlagAPIBuilder,
_ *datasource.DataSourceAPIBuilder,
@@ -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{
+1 -1
View File
@@ -28,7 +28,7 @@ func NewDashboardLargeObjectSupport(scheme *runtime.Scheme) *apistore.BasicLarge
}
old := dash.Spec.Object
spec := commonV0.Unstructured{Object: make(map[string]any)}
dash.Spec = spec
dash.Spec = dashboard.DashboardSpec{Unstructured: spec}
dash.SetManagedFields(nil) // this could be bigger than the object!
keep := []string{"title", "description", "tags", "schemaVersion"}
+1 -1
View File
@@ -53,7 +53,7 @@ func TestLargeDashboardSupport(t *testing.T) {
small, err := json.MarshalIndent(&dash.Spec, "", " ")
require.NoError(t, err)
require.JSONEq(t, `{
"schemaVersion": 33,
"schemaVersion": 41,
"title": "Panel tests - All panels",
"tags": ["gdev","panel-tests","all-panels"]
}`, string(small))
+1 -6
View File
@@ -10,7 +10,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storage"
@@ -25,20 +24,16 @@ type LatestConnector interface {
rest.StorageMetadata
}
func NewLatestConnector(unified resource.ResourceClient, gr schema.GroupResource, opts generic.RESTOptions, scheme *runtime.Scheme) LatestConnector {
func NewLatestConnector(unified resource.ResourceClient, gr schema.GroupResource) LatestConnector {
return &latestREST{
unified: unified,
gr: gr,
opts: opts,
scheme: scheme,
}
}
type latestREST struct {
unified resource.ResourceClient
gr schema.GroupResource
opts generic.RESTOptions
scheme *runtime.Scheme
}
func (l *latestREST) New() runtime.Object {
@@ -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)
+263 -30
View File
@@ -4,77 +4,135 @@ import (
"context"
"errors"
"fmt"
"maps"
"path"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard"
dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
dashboardv1alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v1alpha1"
dashboardv2alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/prometheus/client_golang/prometheus"
)
var (
_ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
_ builder.OpenAPIPostProcessor = (*DashboardsAPIBuilder)(nil)
_ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
_ builder.APIGroupVersionsProvider = (*DashboardsAPIBuilder)(nil)
_ builder.OpenAPIPostProcessor = (*DashboardsAPIBuilder)(nil)
_ builder.APIGroupRouteProvider = (*DashboardsAPIBuilder)(nil)
)
// This is used just so wire has something unique to return
type DashboardsAPIBuilder struct {
ProvisioningDashboardService dashboards.DashboardProvisioningService
dashboardService dashboards.DashboardService
features featuremgmt.FeatureToggles
accessControl accesscontrol.AccessControl
legacy *DashboardStorage
unified resource.ResourceClient
dashboardProvisioningService dashboards.DashboardProvisioningService
scheme *runtime.Scheme
search *SearchHandler
log log.Logger
reg prometheus.Registerer
}
func RegisterAPIService(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
dashboardService dashboards.DashboardService,
provisioningDashboardService dashboards.DashboardProvisioningService,
accessControl accesscontrol.AccessControl,
provisioning provisioning.ProvisioningService,
dashStore dashboards.Store,
reg prometheus.Registerer,
sql db.DB,
tracing *tracing.TracingService,
unified resource.ResourceClient,
dual dualwrite.Service,
sorter sort.Service,
) *DashboardsAPIBuilder {
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
dbp := legacysql.NewDatabaseProvider(sql)
namespacer := request.GetNamespaceMapper(cfg)
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
builder := &DashboardsAPIBuilder{
ProvisioningDashboardService: provisioningDashboardService,
log: log.New("grafana-apiserver.dashboards"),
dashboardService: dashboardService,
features: features,
accessControl: accessControl,
unified: unified,
dashboardProvisioningService: provisioningDashboardService,
search: NewSearchHandler(tracing, dual, legacyDashboardSearcher, unified, features),
legacy: &DashboardStorage{
Resource: dashboardinternal.DashboardResourceInfo,
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
TableConverter: dashboardinternal.DashboardResourceInfo.TableConverter(),
Features: features,
},
reg: reg,
}
apiregistration.RegisterAPI(builder)
return builder
}
func (b *DashboardsAPIBuilder) GetGroupVersion() schema.GroupVersion {
return dashboardinternal.DashboardResourceInfo.GroupVersion()
}
func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return nil // no authorizer
}
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
if err := dashboardinternal.AddToScheme(scheme); err != nil {
return err
}
return scheme.SetVersionPriority(
func (b *DashboardsAPIBuilder) GetGroupVersions() []schema.GroupVersion {
return []schema.GroupVersion{
dashboardv0alpha1.DashboardResourceInfo.GroupVersion(),
dashboardv1alpha1.DashboardResourceInfo.GroupVersion(),
dashboardv2alpha1.DashboardResourceInfo.GroupVersion(),
)
}
}
func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
return nil
}
func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return nil
}
func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
return oas, nil
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
b.scheme = scheme
if err := dashboardinternal.AddToScheme(scheme); err != nil {
return err
}
if err := dashboardv0alpha1.AddToScheme(scheme); err != nil {
return err
}
if err := dashboardv1alpha1.AddToScheme(scheme); err != nil {
return err
}
if err := dashboardv2alpha1.AddToScheme(scheme); err != nil {
return err
}
return scheme.SetVersionPriority(b.GetGroupVersions()...)
}
// Validate will prevent deletion of provisioned dashboards, unless the grace period is set to 0, indicating a force deletion
@@ -93,7 +151,7 @@ func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attribu
return fmt.Errorf("%v: %w", "failed to parse namespace", err)
}
provisioningData, err := b.ProvisioningDashboardService.GetProvisionedDashboardDataByDashboardUID(ctx, nsInfo.OrgID, a.GetName())
provisioningData, err := b.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardUID(ctx, nsInfo.OrgID, a.GetName())
if err != nil {
if errors.Is(err, dashboards.ErrProvisionedDashboardNotFound) {
return nil
@@ -110,3 +168,178 @@ func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attribu
return nil
}
// Mutate removes any internal ID set in the spec & adds it as a label
func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
op := a.GetOperation()
if op != admission.Create && op != admission.Update {
return nil
}
obj := a.GetObject()
dash, ok := obj.(*dashboardinternal.Dashboard)
if !ok {
return fmt.Errorf("mutation error: expected *dashboardinternal.Dashboard, got %T", obj)
}
if id, ok := dash.Spec.Object["id"].(float64); ok {
delete(dash.Spec.Object, "id")
if id != 0 {
meta, err := utils.MetaAccessor(obj)
if err != nil {
return err
}
meta.SetDeprecatedInternalID(int64(id)) // nolint:staticcheck
}
}
return nil
}
func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
internalDashResourceInfo := dashboardinternal.DashboardResourceInfo
legacyStore, err := b.legacy.NewStore(opts.Scheme, opts.OptsGetter, b.reg)
if err != nil {
return err
}
storageOpts := apistore.StorageOptions{
RequireDeprecatedInternalID: true,
}
// Split dashboards when they are large
var largeObjects apistore.LargeObjectSupport
if b.legacy.Features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageBigObjectsSupport) {
largeObjects = NewDashboardLargeObjectSupport(opts.Scheme)
storageOpts.LargeObjectSupport = largeObjects
}
opts.StorageOptions(internalDashResourceInfo.GroupResource(), storageOpts)
// v0alpha1
storage, err := b.storageForVersion(opts, legacyStore, largeObjects, func() runtime.Object {
return &dashboardv0alpha1.DashboardWithAccessInfo{}
})
if err != nil {
return err
}
apiGroupInfo.VersionedResourcesStorageMap[dashboardv0alpha1.VERSION] = storage
// v1alpha1
storage, err = b.storageForVersion(opts, legacyStore, largeObjects, func() runtime.Object {
return &dashboardv1alpha1.DashboardWithAccessInfo{}
})
if err != nil {
return err
}
apiGroupInfo.VersionedResourcesStorageMap[dashboardv1alpha1.VERSION] = storage
// v2alpha1
storage, err = b.storageForVersion(opts, legacyStore, largeObjects, func() runtime.Object {
return &dashboardv2alpha1.DashboardWithAccessInfo{}
})
if err != nil {
return err
}
apiGroupInfo.VersionedResourcesStorageMap[dashboardv2alpha1.VERSION] = storage
return nil
}
func (b *DashboardsAPIBuilder) storageForVersion(
opts builder.APIGroupOptions,
legacyStore grafanarest.LegacyStorage,
largeObjects apistore.LargeObjectSupport,
newDTOFunc func() runtime.Object,
) (map[string]rest.Storage, error) {
var (
err error
scheme = opts.Scheme
dualWriteBuilder = opts.DualWriteBuilder
internalDashResourceInfo = dashboardinternal.DashboardResourceInfo
libraryPanelResourceInfo = dashboardinternal.LibraryPanelResourceInfo
)
storage := map[string]rest.Storage{}
storage[internalDashResourceInfo.StoragePath()] = legacyStore
// Dual writes if a RESTOptionsGetter is provided
if dualWriteBuilder != nil {
store, err := grafanaregistry.NewRegistryStore(scheme, internalDashResourceInfo, opts.OptsGetter)
if err != nil {
return nil, err
}
storage[internalDashResourceInfo.StoragePath()], err = dualWriteBuilder(internalDashResourceInfo.GroupResource(), legacyStore, store)
if err != nil {
return nil, err
}
}
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesRestore) {
storage[internalDashResourceInfo.StoragePath("restore")] = NewRestoreConnector(
b.unified,
internalDashResourceInfo.GroupResource(),
)
storage[internalDashResourceInfo.StoragePath("latest")] = NewLatestConnector(
b.unified,
internalDashResourceInfo.GroupResource(),
)
}
// Register the DTO endpoint that will consolidate all dashboard bits
storage[internalDashResourceInfo.StoragePath("dto")], err = NewDTOConnector(
storage[internalDashResourceInfo.StoragePath()],
largeObjects,
b.legacy.Access,
b.unified,
b.accessControl,
scheme,
newDTOFunc,
)
if err != nil {
return nil, err
}
// Expose read only library panels
storage[libraryPanelResourceInfo.StoragePath()] = &LibraryPanelStore{
Access: b.legacy.Access,
ResourceInfo: dashboardinternal.LibraryPanelResourceInfo,
}
return storage, nil
}
func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
defs := dashboardv0alpha1.GetOpenAPIDefinitions(ref)
maps.Copy(defs, dashboardv1alpha1.GetOpenAPIDefinitions(ref))
maps.Copy(defs, dashboardv2alpha1.GetOpenAPIDefinitions(ref))
return defs
}
}
func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
// The plugin description
oas.Info.Description = "Grafana dashboards as resources"
for _, gv := range b.GetGroupVersions() {
version := gv.Version
// Hide cluster-scoped resources
root := path.Join("/apis/", dashboardinternal.GROUP, version)
delete(oas.Paths.Paths, path.Join(root, "dashboards"))
delete(oas.Paths.Paths, path.Join(root, "watch", "dashboards"))
if version == dashboardv0alpha1.VERSION {
sub := oas.Paths.Paths[path.Join(root, "search", "{name}")]
oas.Paths.Paths[path.Join(root, "search")] = sub
delete(oas.Paths.Paths, path.Join(root, "search", "{name}"))
}
}
return oas, nil
}
func (b *DashboardsAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} })
return b.search.GetAPIRoutes(defs)
}
+1 -1
View File
@@ -128,7 +128,7 @@ func TestDashboardAPIBuilder_Validate(t *testing.T) {
fakeService := &dashboards.FakeDashboardProvisioning{}
fakeService.On("GetProvisionedDashboardDataByDashboardUID", mock.Anything, mock.Anything, mock.Anything).Return(tt.dashboardResponse, tt.dashboardErrorResponse).Once()
b := &DashboardsAPIBuilder{
ProvisioningDashboardService: fakeService,
dashboardProvisioningService: fakeService,
}
err := b.Validate(context.Background(), admission.NewAttributesRecord(
tt.inputObj,
+1 -4
View File
@@ -11,7 +11,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storage"
@@ -25,18 +24,16 @@ type RestoreConnector interface {
rest.StorageMetadata
}
func NewRestoreConnector(unified resource.ResourceClient, gr schema.GroupResource, opts generic.RESTOptions) RestoreConnector {
func NewRestoreConnector(unified resource.ResourceClient, gr schema.GroupResource) RestoreConnector {
return &restoreREST{
unified: unified,
gr: gr,
opts: opts,
}
}
type restoreREST struct {
unified resource.ResourceClient
gr schema.GroupResource
opts generic.RESTOptions
}
func (r *restoreREST) New() runtime.Object {
@@ -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)
})
}
}
-6
View File
@@ -5,9 +5,6 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications"
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
dashboardv0alpha1 "github.com/grafana/grafana/pkg/registry/apis/dashboard/v0alpha1"
dashboardv1alpha1 "github.com/grafana/grafana/pkg/registry/apis/dashboard/v1alpha1"
dashboardv2alpha1 "github.com/grafana/grafana/pkg/registry/apis/dashboard/v2alpha1"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
@@ -30,9 +27,6 @@ var WireSet = wire.NewSet(
// Each must be added here *and* in the ServiceSink above
dashboardinternal.RegisterAPIService,
dashboardv0alpha1.RegisterAPIService,
dashboardv1alpha1.RegisterAPIService,
dashboardv2alpha1.RegisterAPIService,
dashboardsnapshot.RegisterAPIService,
featuretoggle.RegisterAPIService,
datasource.RegisterAPIService,