Snapshots: Migrate API as dashboards k8s subresource (#113552)
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user