Snapshots: Migrate API as dashboards k8s subresource (#113552)

This commit is contained in:
Ezequiel Victorero
2025-12-02 16:26:45 -03:00
committed by GitHub
parent 2ef211fe96
commit 227b596a46
55 changed files with 2717 additions and 1813 deletions
+2
View File
@@ -33,6 +33,8 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute
case dashboardV0.LIBRARY_PANEL_RESOURCE:
return nil // nothing needed
case dashboardV0.SNAPSHOT_RESOURCE:
return nil
}
return fmt.Errorf("unexpected resource: %+v", a.GetResource())
+45 -4
View File
@@ -40,6 +40,7 @@ import (
"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/registry/apis/dashboard/snapshot"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver"
grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
@@ -48,6 +49,7 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/libraryelements"
@@ -114,8 +116,10 @@ type DashboardsAPIBuilder struct {
folderClientProvider client.K8sHandlerProvider
libraryPanels libraryelements.Service // for legacy library panels
publicDashboardService publicdashboards.Service
isStandalone bool // skips any handling including anything to do with legacy storage
snapshotService dashboardsnapshots.Service
snapshotOptions dashv0.SnapshotSharingOptions
namespacer request.NamespaceMapper
isStandalone bool // skips any handling including anything to do with legacy storage
}
func RegisterAPIService(
@@ -143,12 +147,20 @@ func RegisterAPIService(
userService user.Service,
libraryPanels libraryelements.Service,
publicDashboardService publicdashboards.Service,
snapshotService dashboardsnapshots.Service,
) *DashboardsAPIBuilder {
dbp := legacysql.NewDatabaseProvider(sql)
namespacer := request.GetNamespaceMapper(cfg)
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
folderClient := client.NewK8sHandler(dual, request.GetNamespaceMapper(cfg), folders.FolderResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashStore, userService, unified, sorter, features)
snapshotOptions := dashv0.SnapshotSharingOptions{
SnapshotsEnabled: cfg.SnapshotEnabled,
ExternalSnapshotURL: cfg.ExternalSnapshotUrl,
ExternalSnapshotName: cfg.ExternalSnapshotName,
ExternalEnabled: cfg.ExternalEnabled,
}
builder := &DashboardsAPIBuilder{
dashboardService: dashboardService,
dashboardPermissions: dashboardPermissions,
@@ -167,7 +179,9 @@ func RegisterAPIService(
folderClientProvider: newSimpleFolderClientProvider(folderClient),
libraryPanels: libraryPanels,
publicDashboardService: publicDashboardService,
snapshotService: snapshotService,
snapshotOptions: snapshotOptions,
namespacer: namespacer,
legacy: &DashboardStorage{
Access: legacy.NewDashboardSQLAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features),
DashboardService: dashboardService,
@@ -244,6 +258,7 @@ func (b *DashboardsAPIBuilder) AllowedV0Alpha1Resources() []string {
return []string{
dashv0.DashboardKind().Plural(),
dashv0.LIBRARY_PANEL_RESOURCE,
dashv0.SNAPSHOT_RESOURCE,
}
}
@@ -267,6 +282,8 @@ func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attribu
case dashv0.LIBRARY_PANEL_RESOURCE:
return nil // OK for now
case dashv0.SNAPSHOT_RESOURCE:
return nil // OK for now
}
return fmt.Errorf("unsupported validation: %+v", a.GetResource())
@@ -535,6 +552,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
if err := b.storageForVersion(apiGroupInfo, opts, largeObjects,
dashv0.DashboardResourceInfo,
&dashv0.LibraryPanelResourceInfo,
&dashv0.SnapshotResourceInfo,
func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) {
dto := &dashv0.DashboardWithAccessInfo{}
dash, ok := obj.(*dashv0.Dashboard)
@@ -553,6 +571,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
if err := b.storageForVersion(apiGroupInfo, opts, largeObjects,
dashv1.DashboardResourceInfo,
nil, // do not register library panel
nil,
func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) {
dto := &dashv1.DashboardWithAccessInfo{}
dash, ok := obj.(*dashv1.Dashboard)
@@ -571,6 +590,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
if err := b.storageForVersion(apiGroupInfo, opts, largeObjects,
dashv2alpha1.DashboardResourceInfo,
nil, // do not register library panel
nil,
func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) {
dto := &dashv2alpha1.DashboardWithAccessInfo{}
dash, ok := obj.(*dashv2alpha1.Dashboard)
@@ -588,6 +608,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
if err := b.storageForVersion(apiGroupInfo, opts, largeObjects,
dashv2beta1.DashboardResourceInfo,
nil, // do not register library panel
nil,
func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) {
dto := &dashv2beta1.DashboardWithAccessInfo{}
dash, ok := obj.(*dashv2beta1.Dashboard)
@@ -611,6 +632,7 @@ func (b *DashboardsAPIBuilder) storageForVersion(
largeObjects apistore.LargeObjectSupport,
dashboards utils.ResourceInfo,
libraryPanels *utils.ResourceInfo,
snapshots *utils.ResourceInfo,
newDTOFunc dtoBuilder,
) error {
// Register the versioned storage
@@ -685,6 +707,20 @@ func (b *DashboardsAPIBuilder) storageForVersion(
}
}
// Legacy only (for now) and only v0alpha1
if snapshots != nil && dashboards.GroupVersion().Version == "v0alpha1" {
snapshotLegacyStore := &snapshot.SnapshotLegacyStore{
ResourceInfo: *snapshots,
Service: b.snapshotService,
Namespacer: b.namespacer,
Options: b.snapshotOptions,
}
storage[snapshots.StoragePath()] = snapshotLegacyStore
storage[snapshots.StoragePath("dashboard")], err = snapshot.NewDashboardREST(dashboards, b.snapshotService)
if err != nil {
return err
}
}
return nil
}
@@ -782,7 +818,12 @@ func (b *DashboardsAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.API
}
defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} })
return b.search.GetAPIRoutes(defs)
searchAPIRoutes := b.search.GetAPIRoutes(defs)
snapshotAPIRoutes := snapshot.GetRoutes(b.snapshotService, b.snapshotOptions, defs)
return &builder.APIRoutes{
Namespace: append(searchAPIRoutes.Namespace, snapshotAPIRoutes.Namespace...),
}
}
// The default authorizer is fine because authorization happens in storage where we know the parent folder
@@ -0,0 +1,80 @@
package snapshot
import (
"fmt"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
dashV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
)
func convertSnapshotDTOToK8sResource(v *dashboardsnapshots.DashboardSnapshotDTO, namespacer request.NamespaceMapper) *dashV0.Snapshot {
expires := v.Expires.UnixMilli()
if v.Expires.After(time.Date(2070, time.January, 0, 0, 0, 0, 0, time.UTC)) {
expires = 0 // ignore things expiring long into the future
}
snap := &dashV0.Snapshot{
TypeMeta: dashV0.SnapshotResourceInfo.TypeMeta(),
ObjectMeta: metav1.ObjectMeta{
Name: v.Key,
ResourceVersion: fmt.Sprintf("%d", v.Updated.UnixMilli()),
CreationTimestamp: metav1.NewTime(v.Created),
Namespace: namespacer(v.OrgID),
},
Spec: dashV0.SnapshotSpec{
Title: &v.Name,
},
}
// Only show external settings when it is external
if v.External {
snap.Spec.External = &v.External
snap.Spec.ExternalUrl = &v.ExternalURL
}
if expires > 0 {
snap.Spec.Expires = &expires
}
if v.Updated != v.Created {
meta, _ := utils.MetaAccessor(snap)
meta.SetUpdatedTimestamp(&v.Updated)
}
return snap
}
func convertSnapshotToK8sResource(v *dashboardsnapshots.DashboardSnapshot, namespacer request.NamespaceMapper) *dashV0.Snapshot {
expires := v.Expires.UnixMilli()
if v.Expires.After(time.Date(2070, time.January, 0, 0, 0, 0, 0, time.UTC)) {
expires = 0 // ignore things expiring long into the future
}
snap := &dashV0.Snapshot{
ObjectMeta: metav1.ObjectMeta{
Name: v.Key,
ResourceVersion: fmt.Sprintf("%d", v.Updated.UnixMilli()),
CreationTimestamp: metav1.NewTime(v.Created),
Namespace: namespacer(v.OrgID),
},
Spec: dashV0.SnapshotSpec{
Title: &v.Name,
},
}
// Only show external settings when it is external
if v.External {
snap.Spec.External = &v.External
snap.Spec.ExternalUrl = &v.ExternalURL
}
if expires > 0 {
snap.Spec.Expires = &expires
}
if v.Updated != v.Created {
meta, _ := utils.MetaAccessor(snap)
meta.SetUpdatedTimestamp(&v.Updated)
}
return snap
}
@@ -0,0 +1,171 @@
package snapshot
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
authlib "github.com/grafana/authlib/types"
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errhttp"
"github.com/grafana/grafana/pkg/web"
)
func GetRoutes(service dashboardsnapshots.Service, options dashv0.SnapshotSharingOptions, defs map[string]common.OpenAPIDefinition) *builder.APIRoutes {
prefix := dashv0.SnapshotResourceInfo.GroupResource().Resource
tags := []string{dashv0.SnapshotResourceInfo.GroupVersionKind().Kind}
createCmd := defs["github.com/grafana/grafana/apps/dashboard/pkg/apissnapshot/v0alpha1.DashboardCreateCommand"].Schema
createExample := `{"dashboard":{"annotations":{"list":[{"name":"Annotations & Alerts","enable":true,"iconColor":"rgba(0, 211, 255, 1)","snapshotData":[],"type":"dashboard","builtIn":1,"hide":true}]},"editable":true,"fiscalYearStartMonth":0,"graphTooltip":0,"id":203,"links":[],"liveNow":false,"panels":[{"datasource":null,"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisBorderShow":false,"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":43,"gradientMode":"opacity","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unitScale":true},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":0},"id":1,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"10.4.0-pre","snapshotData":[{"fields":[{"config":{"color":{"mode":"palette-classic"},"custom":{"axisBorderShow":false,"axisCenteredZero":false,"axisColorMode":"text","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":43,"gradientMode":"opacity","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"smooth","lineWidth":1,"pointSize":5,"showPoints":"auto","thresholdsStyle":{"mode":"off"}},"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unitScale":true},"name":"time","type":"time","values":[1706030536378,1706034856378,1706039176378,1706043496378,1706047816378,1706052136378]},{"config":{"color":{"mode":"palette-classic"},"custom":{"axisBorderShow":false,"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":43,"gradientMode":"opacity","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unitScale":true},"name":"A-series","type":"number","values":[1,20,90,30,50,0]}],"refId":"A"}],"targets":[],"title":"Simple example","type":"timeseries","links":[]}],"refresh":"","schemaVersion":39,"snapshot":{"timestamp":"2024-01-23T23:22:16.377Z"},"tags":[],"templating":{"list":[]},"time":{"from":"2024-01-23T17:22:20.380Z","to":"2024-01-23T23:22:20.380Z","raw":{"from":"now-6h","to":"now"}},"timepicker":{},"timezone":"","title":"simple and small","uid":"b22ec8db-399b-403b-b6c7-b0fb30ccb2a5","version":1,"weekStart":""},"name":"simple and small","expires":86400}`
createRsp := defs["github.com/grafana/grafana/apps/dashboard/pkg/apissnapshot/v0alpha1.DashboardCreateResponse"].Schema
return &builder.APIRoutes{
Namespace: []builder.APIRouteHandler{
{
Path: prefix + "/create",
Spec: &spec3.PathProps{
Post: &spec3.Operation{
VendorExtensible: spec.VendorExtensible{
Extensions: map[string]any{
"x-grafana-action": "create",
"x-kubernetes-group-version-kind": metav1.GroupVersionKind{
Group: dashv0.GROUP,
Version: dashv0.VERSION,
Kind: "DashboardCreateResponse",
},
},
},
OperationProps: spec3.OperationProps{
Tags: tags,
OperationId: "createSnapshot",
Description: "Creates a new Snapshot",
Parameters: []*spec3.Parameter{
{
ParameterProps: spec3.ParameterProps{
Name: "namespace",
In: "path",
Required: true,
Example: "default",
Description: "workspace",
Schema: spec.StringProperty(),
},
},
},
RequestBody: &spec3.RequestBody{
RequestBodyProps: spec3.RequestBodyProps{
Content: map[string]*spec3.MediaType{
"application/json": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: &createCmd,
Example: createExample, // raw JSON body
},
},
},
},
},
Responses: &spec3.Responses{
ResponsesProps: spec3.ResponsesProps{
StatusCodeResponses: map[int]*spec3.Response{
200: {
ResponseProps: spec3.ResponseProps{
Content: map[string]*spec3.MediaType{
"application/json": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: &createRsp,
},
},
},
},
},
},
},
},
},
},
},
Handler: func(w http.ResponseWriter, r *http.Request) {
user, err := identity.GetRequester(r.Context())
if err != nil {
errhttp.Write(r.Context(), err, w)
return
}
wrap := &contextmodel.ReqContext{
Context: &web.Context{
Req: r,
Resp: web.NewResponseWriter(r.Method, w),
},
// SignedInUser: user, ????????????
}
vars := mux.Vars(r)
info, err := authlib.ParseNamespace(vars["namespace"])
if err != nil {
wrap.JsonApiErr(http.StatusBadRequest, "expected namespace", nil)
return
}
if info.OrgID != user.GetOrgID() {
wrap.JsonApiErr(http.StatusBadRequest,
fmt.Sprintf("user orgId does not match namespace (%d != %d)", info.OrgID, user.GetOrgID()), nil)
return
}
cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{}
if err := web.Bind(wrap.Req, &cmd); err != nil {
wrap.JsonApiErr(http.StatusBadRequest, "bad request data", err)
return
}
// Use the existing snapshot service
dashboardsnapshots.CreateDashboardSnapshot(wrap, options, cmd, service)
},
},
{
Path: prefix + "/delete/{deleteKey}",
Spec: &spec3.PathProps{
Description: "Delete snapshot by delete key",
Delete: &spec3.Operation{
OperationProps: spec3.OperationProps{
Tags: tags,
OperationId: "deleteWithKey",
Parameters: []*spec3.Parameter{
{
ParameterProps: spec3.ParameterProps{
Name: "deleteKey",
In: "path",
Required: true,
Description: "unique key returned in create",
Schema: spec.StringProperty(),
},
},
},
},
},
},
Handler: func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
key := vars["deleteKey"]
err := dashboardsnapshots.DeleteWithKey(ctx, key, service)
if err != nil {
errhttp.Write(ctx, fmt.Errorf("failed to delete external dashboard (%w)", err), w)
return
}
_ = json.NewEncoder(w).Encode(&util.DynMap{
"message": "Snapshot deleted. It might take an hour before it's cleared from any CDN caches.",
})
},
},
}}
}
@@ -0,0 +1,149 @@
package snapshot
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
dashV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
)
var (
_ rest.Scoper = (*SnapshotLegacyStore)(nil)
_ rest.SingularNameProvider = (*SnapshotLegacyStore)(nil)
_ rest.Getter = (*SnapshotLegacyStore)(nil)
_ rest.Lister = (*SnapshotLegacyStore)(nil)
_ rest.GracefulDeleter = (*SnapshotLegacyStore)(nil)
_ rest.Storage = (*SnapshotLegacyStore)(nil)
)
type SnapshotLegacyStore struct {
ResourceInfo utils.ResourceInfo
Service dashboardsnapshots.Service
Namespacer request.NamespaceMapper
Options dashV0.SnapshotSharingOptions
}
func (s *SnapshotLegacyStore) New() runtime.Object {
return s.ResourceInfo.NewFunc()
}
func (s *SnapshotLegacyStore) Destroy() {}
func (s *SnapshotLegacyStore) NamespaceScoped() bool {
return true // namespace == org
}
func (s *SnapshotLegacyStore) GetSingularName() string {
return s.ResourceInfo.GetSingularName()
}
func (s *SnapshotLegacyStore) NewList() runtime.Object {
return s.ResourceInfo.NewListFunc()
}
func (s *SnapshotLegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return s.ResourceInfo.TableConverter().ConvertToTable(ctx, object, tableOptions)
}
// GracefulDeleter
func (s *SnapshotLegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
snap, err := s.Service.GetDashboardSnapshot(ctx, &dashboardsnapshots.GetDashboardSnapshotQuery{
Key: name,
})
if err != nil || snap == nil {
return nil, false, err
}
// Delete the external one first
if snap.ExternalDeleteURL != "" {
err := dashboardsnapshots.DeleteExternalDashboardSnapshot(snap.ExternalDeleteURL)
if err != nil {
return nil, false, err
}
}
err = s.Service.DeleteDashboardSnapshot(ctx, &dashboardsnapshots.DeleteDashboardSnapshotCommand{
DeleteKey: snap.DeleteKey,
})
if err != nil {
return nil, false, err
}
return nil, true, nil
}
func (s *SnapshotLegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
orgId, err := request.OrgIDForList(ctx)
if err != nil {
return nil, err
}
requester, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
limit := options.Limit
if limit < 1 {
limit = 1000
}
searchQuery := dashboardsnapshots.GetDashboardSnapshotsQuery{
Name: "", // TODO: Should we support searching by name? In the levacy api is a string query param called query
Limit: int(limit),
OrgID: orgId,
SignedInUser: requester,
}
res, err := s.Service.SearchDashboardSnapshots(ctx, &searchQuery)
if err != nil {
return nil, err
}
list := &dashV0.SnapshotList{}
//convert
for idx := range res {
list.Items = append(list.Items, *convertSnapshotDTOToK8sResource(res[idx], s.Namespacer))
}
return list, nil
}
func (s *SnapshotLegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
err = s.checkEnabled(info.Value)
if err != nil {
return nil, err
}
query := dashboardsnapshots.GetDashboardSnapshotQuery{
Key: name,
}
res, err := s.Service.GetDashboardSnapshot(ctx, &query)
if err != nil {
return nil, err
}
if res != nil {
return convertSnapshotToK8sResource(res, s.Namespacer), nil
}
return nil, s.ResourceInfo.NewNotFound(name)
}
func (s *SnapshotLegacyStore) checkEnabled(ns string) error {
if !s.Options.SnapshotsEnabled {
return fmt.Errorf("snapshots not enabled")
}
return nil
}
@@ -0,0 +1,81 @@
package snapshot
import (
"context"
"net/http"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
)
// Currently only works with v0alpha1
type dashboardREST struct {
Service dashboardsnapshots.Service
}
func NewDashboardREST(
resourceInfo utils.ResourceInfo,
service dashboardsnapshots.Service,
) (rest.Storage, error) {
return &dashboardREST{
Service: service,
}, nil
}
var (
_ rest.Connecter = (*dashboardREST)(nil)
_ rest.StorageMetadata = (*dashboardREST)(nil)
)
func (r *dashboardREST) New() runtime.Object {
return &dashv0.Dashboard{}
}
func (r *dashboardREST) Destroy() {
}
func (r *dashboardREST) ConnectMethods() []string {
return []string{"GET"}
}
func (r *dashboardREST) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, ""
}
func (r *dashboardREST) ProducesMIMETypes(verb string) []string {
return nil
}
func (r *dashboardREST) ProducesObject(verb string) interface{} {
return r.New()
}
func (r *dashboardREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
_, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
snap, err := r.Service.GetDashboardSnapshot(ctx, &dashboardsnapshots.GetDashboardSnapshotQuery{Key: name})
if err != nil {
return nil, err
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// TODO... support conversions (not required in v0)
dash := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: name,
},
Spec: v0alpha1.Unstructured{
Object: snap.Dashboard.MustMap(),
},
}
responder.Object(200, dash)
}), nil
}