mirror of https://github.com/grafana/grafana
Dashboard: Multi-version builder (#100305)
parent
7be1fd953a
commit
3992ac2ac1
@ -1,34 +0,0 @@ |
||||
package v2alpha1 |
||||
|
||||
import ( |
||||
conversion "k8s.io/apimachinery/pkg/conversion" |
||||
klog "k8s.io/klog/v2" |
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/apis/dashboard/migration" |
||||
"github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" |
||||
) |
||||
|
||||
func Convert_v0alpha1_Unstructured_To_v2alpha1_DashboardSpec(in *common.Unstructured, out *DashboardSpec, s conversion.Scope) error { |
||||
out.Unstructured = *in |
||||
err := migration.Migrate(out.Unstructured.Object, schemaversion.LATEST_VERSION) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
t, ok := out.Unstructured.Object["title"].(string) |
||||
if !ok { |
||||
klog.V(5).Infof("unstructured dashboard title field is not a string %v", t) |
||||
return nil // skip setting the title if it's not a string in the unstructured object
|
||||
} |
||||
out.Title = t |
||||
return nil |
||||
} |
||||
|
||||
func Convert_v2alpha1_DashboardSpec_To_v0alpha1_Unstructured(in *DashboardSpec, out *common.Unstructured, s conversion.Scope) error { |
||||
*out = in.Unstructured |
||||
if in.Title != "" { |
||||
out.Object["title"] = in.Title |
||||
} |
||||
return nil |
||||
} |
||||
@ -1,67 +0,0 @@ |
||||
package v2alpha1 |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" |
||||
) |
||||
|
||||
func TestConvertDashboardVersions(t *testing.T) { |
||||
dashboardV0Spec := []byte(`{ |
||||
"annotations": { |
||||
"list": [ |
||||
{ |
||||
"builtIn": 1, |
||||
"datasource": { |
||||
"type": "grafana", |
||||
"uid": "-- Grafana --" |
||||
}, |
||||
"enable": true, |
||||
"hide": true, |
||||
"iconColor": "rgba(0, 211, 255, 1)", |
||||
"name": "Annotations \u0026 Alerts", |
||||
"type": "dashboard" |
||||
} |
||||
] |
||||
}, |
||||
"refresh": true, |
||||
"description": "", |
||||
"editable": true, |
||||
"fiscalYearStartMonth": 0, |
||||
"graphTooltip": 0, |
||||
"id": 11711, |
||||
"links": [], |
||||
"panels": [], |
||||
"preload": false, |
||||
"schemaVersion": 39, |
||||
"tags": [], |
||||
"templating": { |
||||
"list": [] |
||||
}, |
||||
"timepicker": {}, |
||||
"timezone": "utc", |
||||
"title": "New dashboard", |
||||
"uid": "be3ymutzclgqod", |
||||
"version": 1, |
||||
"weekStart": "" |
||||
}`) |
||||
object := common.Unstructured{} |
||||
err := json.Unmarshal(dashboardV0Spec, &object.Object) |
||||
require.NoError(t, err) |
||||
result := DashboardSpec{} |
||||
// convert v0 to v2, where we should extract the title & all other elements should be copied
|
||||
err = Convert_v0alpha1_Unstructured_To_v2alpha1_DashboardSpec(&object, &result, nil) |
||||
require.NoError(t, err) |
||||
require.Equal(t, result.Title, "New dashboard") |
||||
require.Equal(t, result.Unstructured, object) |
||||
require.Equal(t, result.Unstructured.Object["refresh"], "", "schemaVersion migration not applied. refresh should be an empty string") |
||||
|
||||
// now convert back & ensure it is the same
|
||||
object2 := common.Unstructured{} |
||||
err = Convert_v2alpha1_DashboardSpec_To_v0alpha1_Unstructured(&result, &object2, nil) |
||||
require.NoError(t, err) |
||||
require.Equal(t, object, object2) |
||||
} |
||||
@ -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) |
||||
}) |
||||
} |
||||
} |
||||
@ -1,34 +0,0 @@ |
||||
package apistore |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource" |
||||
"github.com/stretchr/testify/require" |
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
) |
||||
|
||||
func TestStreamDecoder(t *testing.T) { |
||||
t.Run("toObject should handle internal conversion", func(t *testing.T) { |
||||
called := false |
||||
internalConversion := func(data []byte, obj runtime.Object) (runtime.Object, error) { |
||||
called = true |
||||
return obj, nil |
||||
} |
||||
|
||||
decoder := &streamDecoder{ |
||||
newFunc: func() runtime.Object { return &unstructured.Unstructured{} }, |
||||
internalConversion: internalConversion, |
||||
} |
||||
|
||||
event := &resource.WatchEvent_Resource{ |
||||
Value: []byte("test"), |
||||
} |
||||
|
||||
obj, err := decoder.toObject(event) |
||||
require.NoError(t, err) |
||||
require.NotNil(t, obj) |
||||
require.True(t, called, "internal conversion function should have been called") |
||||
}) |
||||
} |
||||
Loading…
Reference in new issue