Dashboard: Multi-version builder (#100305)
This commit is contained 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,
|
||||
|
||||
Reference in New Issue
Block a user