K8s/Snapshots: Add dashboardsnapshot api group (#77667)

This commit is contained in:
Ryan McKinley
2024-02-01 22:40:11 -08:00
committed by GitHub
parent 810d14d88f
commit 795eb4a8d8
32 changed files with 2543 additions and 267 deletions
+2
View File
@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/registry/apis/example"
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
@@ -26,6 +27,7 @@ func ProvideRegistryServiceSink(
_ *dashboard.DashboardsAPIBuilder,
_ *playlist.PlaylistAPIBuilder,
_ *example.TestingAPIBuilder,
_ *dashboardsnapshot.SnapshotsAPIBuilder,
_ *featuretoggle.FeatureFlagAPIBuilder,
_ *datasource.DataSourceAPIBuilder,
_ *folders.FolderAPIBuilder,
@@ -0,0 +1,71 @@
package dashboardsnapshot
import (
"fmt"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
)
func convertDTOToSnapshot(v *dashboardsnapshots.DashboardSnapshotDTO, namespacer request.NamespaceMapper) *dashboardsnapshot.DashboardSnapshot {
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 := &dashboardsnapshot.DashboardSnapshot{
TypeMeta: resourceInfo.TypeMeta(),
ObjectMeta: metav1.ObjectMeta{
Name: v.Key,
ResourceVersion: fmt.Sprintf("%d", v.Updated.UnixMilli()),
CreationTimestamp: metav1.NewTime(v.Created),
Namespace: namespacer(v.OrgID),
},
Spec: dashboardsnapshot.SnapshotInfo{
Title: v.Name,
ExternalURL: v.ExternalURL,
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) *dashboardsnapshot.DashboardSnapshot {
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
}
info := dashboardsnapshot.SnapshotInfo{
Title: v.Name,
ExternalURL: v.ExternalURL,
Expires: expires,
}
s := v.Dashboard.Get("snapshot")
if s != nil {
info.OriginalUrl, _ = s.Get("originalUrl").String()
info.Timestamp, _ = s.Get("timestamp").String()
}
snap := &dashboardsnapshot.DashboardSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: v.Key,
ResourceVersion: fmt.Sprintf("%d", v.Updated.UnixMilli()),
CreationTimestamp: metav1.NewTime(v.Created),
Namespace: namespacer(v.OrgID),
},
Spec: info,
}
if v.Updated != v.Created {
meta, _ := utils.MetaAccessor(snap)
meta.SetUpdatedTimestamp(&v.Updated)
}
return snap
}
@@ -0,0 +1,130 @@
package dashboardsnapshot
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"gocloud.dev/blob"
"k8s.io/kube-openapi/pkg/spec3"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
)
type dashExportStatus struct {
Count int
Index int
Started int64
Updated int64
Finished int64
Error string
}
type dashExporter struct {
status dashExportStatus
service dashboardsnapshots.Service
sql db.DB
}
func (d *dashExporter) getAPIRouteHandler() builder.APIRouteHandler {
return builder.APIRouteHandler{
Path: "admin/export",
Spec: &spec3.PathProps{
Summary: "an example at the root level",
Description: "longer description here?",
Post: &spec3.Operation{
OperationProps: spec3.OperationProps{
Tags: []string{"export"},
Responses: &spec3.Responses{
ResponsesProps: spec3.ResponsesProps{
StatusCodeResponses: map[int]*spec3.Response{
200: {
ResponseProps: spec3.ResponseProps{
Content: map[string]*spec3.MediaType{
"application/json": {},
},
},
},
},
},
},
},
},
},
Handler: func(w http.ResponseWriter, r *http.Request) {
// Only let it start once
if d.status.Started == 0 {
go d.doExport()
}
time.Sleep(time.Second)
_ = json.NewEncoder(w).Encode(d.status)
},
}
}
// NO way to stop!!!!!!
func (d *dashExporter) doExport() {
defer func() {
d.status.Finished = time.Now().UnixMilli()
}()
d.status = dashExportStatus{
Started: time.Now().UnixMilli(),
}
if d.sql == nil {
d.status.Error = "missing dependencies"
return
}
ctx := context.Background()
keys := []string{}
err := d.sql.GetSqlxSession().Select(ctx,
&keys, "SELECT key FROM dashboard_snapshot ORDER BY id asc")
if err != nil {
d.status.Error = err.Error()
return
}
d.status.Count = len(keys)
bucket, err := blob.OpenBucket(ctx, "mem://?key=foo.txt&prefix=a/subfolder/")
if err != nil {
d.status.Error = err.Error()
return
}
defer func() {
_ = bucket.Close()
}()
for idx, key := range keys {
d.status.Index = idx
snap, err := d.service.GetDashboardSnapshot(ctx, &dashboardsnapshots.GetDashboardSnapshotQuery{
Key: key,
})
if err != nil {
d.status.Error = err.Error()
return
}
dash, err := snap.Dashboard.ToDB()
if err != nil {
d.status.Error = err.Error()
return
}
fmt.Printf("TODO, export: %s (len: %d)\n", snap.Key, len(dash))
// w, err := bucket.NewWriter(ctx, "foo.txt", nil)
// if err != nil {
// d.status.Error = err.Error()
// return
// }
time.Sleep(time.Second * 1)
d.status.Updated = time.Now().UnixMilli()
}
fmt.Printf("done!\n")
}
@@ -0,0 +1,91 @@
package dashboardsnapshot
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"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/setting"
)
var (
_ rest.Scoper = (*optionsStorage)(nil)
_ rest.SingularNameProvider = (*optionsStorage)(nil)
_ rest.Getter = (*optionsStorage)(nil)
_ rest.Lister = (*optionsStorage)(nil)
_ rest.Storage = (*optionsStorage)(nil)
)
type sharingOptionsGetter = func(namespace string) (*dashboardsnapshot.SharingOptions, error)
func newSharingOptionsGetter(cfg *setting.Cfg) sharingOptionsGetter {
s := &dashboardsnapshot.SharingOptions{
ObjectMeta: metav1.ObjectMeta{
CreationTimestamp: metav1.Now(),
},
Spec: dashboardsnapshot.SnapshotSharingOptions{
SnapshotsEnabled: cfg.SnapshotEnabled,
ExternalSnapshotURL: cfg.ExternalSnapshotUrl,
ExternalSnapshotName: cfg.ExternalSnapshotName,
ExternalEnabled: cfg.ExternalEnabled,
},
}
return func(namespace string) (*dashboardsnapshot.SharingOptions, error) {
return s, nil
}
}
type optionsStorage struct {
getter sharingOptionsGetter
tableConverter rest.TableConvertor
}
func (s *optionsStorage) New() runtime.Object {
return &dashboardsnapshot.SharingOptions{}
}
func (s *optionsStorage) Destroy() {}
func (s *optionsStorage) NamespaceScoped() bool {
return true
}
func (s *optionsStorage) GetSingularName() string {
return "options"
}
func (s *optionsStorage) NewList() runtime.Object {
return &dashboardsnapshot.SharingOptionsList{}
}
func (s *optionsStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
}
func (s *optionsStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
if info.OrgID < 0 {
return nil, fmt.Errorf("missing namespace")
}
v, err := s.getter(info.Value)
if err != nil {
return nil, err
}
list := &dashboardsnapshot.SharingOptionsList{
Items: []dashboardsnapshot.SharingOptions{*v},
}
return list, nil
}
func (s *optionsStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return s.getter(name)
}
@@ -0,0 +1,345 @@
package dashboardsnapshot
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/gorilla/mux"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
common "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/apiserver/utils"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil/errhttp"
"github.com/grafana/grafana/pkg/web"
)
var _ builder.APIGroupBuilder = (*SnapshotsAPIBuilder)(nil)
var resourceInfo = dashboardsnapshot.DashboardSnapshotResourceInfo
// This is used just so wire has something unique to return
type SnapshotsAPIBuilder struct {
service dashboardsnapshots.Service
namespacer request.NamespaceMapper
options sharingOptionsGetter
exporter *dashExporter
logger log.Logger
}
func NewSnapshotsAPIBuilder(
p dashboardsnapshots.Service,
cfg *setting.Cfg,
exporter *dashExporter,
) *SnapshotsAPIBuilder {
return &SnapshotsAPIBuilder{
service: p,
options: newSharingOptionsGetter(cfg),
namespacer: request.GetNamespaceMapper(cfg),
exporter: exporter,
logger: log.New("snapshots::RawHandlers"),
}
}
func RegisterAPIService(
service dashboardsnapshots.Service,
apiregistration builder.APIRegistrar,
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
sql db.DB,
) *SnapshotsAPIBuilder {
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
return nil // skip registration unless opting into experimental apis
}
builder := NewSnapshotsAPIBuilder(service, cfg, &dashExporter{
service: service,
sql: sql,
})
apiregistration.RegisterAPI(builder)
return builder
}
func (b *SnapshotsAPIBuilder) GetGroupVersion() schema.GroupVersion {
return resourceInfo.GroupVersion()
}
func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
scheme.AddKnownTypes(gv,
&dashboardsnapshot.DashboardSnapshot{},
&dashboardsnapshot.DashboardSnapshotList{},
&dashboardsnapshot.SharingOptions{},
&dashboardsnapshot.SharingOptionsList{},
&dashboardsnapshot.FullDashboardSnapshot{},
&dashboardsnapshot.DashboardSnapshotWithDeleteKey{},
&metav1.Status{},
)
}
func (b *SnapshotsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
gv := resourceInfo.GroupVersion()
addKnownTypes(scheme, gv)
// Link this version to the internal representation.
// This is used for server-side-apply (PATCH), and avoids the error:
// "no kind is registered for the type"
addKnownTypes(scheme, schema.GroupVersion{
Group: gv.Group,
Version: runtime.APIVersionInternal,
})
// If multiple versions exist, then register conversions from zz_generated.conversion.go
// if err := playlist.RegisterConversions(scheme); err != nil {
// return err
// }
metav1.AddToGroupVersion(scheme, gv)
return scheme.SetVersionPriority(gv)
}
func (b *SnapshotsAPIBuilder) GetAPIGroupInfo(
scheme *runtime.Scheme,
codecs serializer.CodecFactory, // pointer?
optsGetter generic.RESTOptionsGetter,
dualWrite bool,
) (*genericapiserver.APIGroupInfo, error) {
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(dashboardsnapshot.GROUP, scheme, metav1.ParameterCodec, codecs)
storage := map[string]rest.Storage{}
legacyStore := &legacyStorage{
service: b.service,
namespacer: b.namespacer,
options: b.options,
}
legacyStore.tableConverter = utils.NewTableConverter(
resourceInfo.GroupResource(),
[]metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The snapshot name"},
{Name: "Created At", Type: "date"},
},
func(obj any) ([]interface{}, error) {
m, ok := obj.(*dashboardsnapshot.DashboardSnapshot)
if ok {
return []interface{}{
m.Name,
m.Spec.Title,
m.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
}
return nil, fmt.Errorf("expected snapshot")
},
)
storage[resourceInfo.StoragePath()] = legacyStore
storage[resourceInfo.StoragePath("body")] = &subBodyREST{
service: b.service,
namespacer: b.namespacer,
}
storage["options"] = &optionsStorage{
getter: b.options,
tableConverter: legacyStore.tableConverter,
}
apiGroupInfo.VersionedResourcesStorageMap[dashboardsnapshot.VERSION] = storage
return &apiGroupInfo, nil
}
func (b *SnapshotsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return dashboardsnapshot.GetOpenAPIDefinitions
}
// Register additional routes with the server
func (b *SnapshotsAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
prefix := dashboardsnapshot.DashboardSnapshotResourceInfo.GroupResource().Resource
defs := dashboardsnapshot.GetOpenAPIDefinitions(func(path string) spec.Ref { return spec.Ref{} })
createCmd := defs["github.com/grafana/grafana/pkg/apis/dashboardsnapshot/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/pkg/apis/dashboardsnapshot/v0alpha1.DashboardCreateResponse"].Schema
tags := []string{dashboardsnapshot.DashboardSnapshotResourceInfo.GroupVersionKind().Kind}
routes := &builder.APIRoutes{
Namespace: []builder.APIRouteHandler{
{
Path: prefix + "/create",
Spec: &spec3.PathProps{
Summary: "an example at the root level",
Description: "longer description here?",
Post: &spec3.Operation{
OperationProps: spec3.OperationProps{
Tags: tags,
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 := appcontext.User(r.Context())
if err != nil {
errhttp.Write(r.Context(), err, w)
return
}
wrap := &contextmodel.ReqContext{
Logger: b.logger,
Context: &web.Context{
Req: r,
Resp: web.NewResponseWriter(r.Method, w),
},
SignedInUser: user,
}
vars := mux.Vars(r)
info, err := request.ParseNamespace(vars["namespace"])
if err != nil {
wrap.JsonApiErr(http.StatusBadRequest, "expected namespace", nil)
return
}
if info.OrgID != user.OrgID {
wrap.JsonApiErr(http.StatusBadRequest,
fmt.Sprintf("user orgId does not match namespace (%d != %d)", info.OrgID, user.OrgID), nil)
return
}
opts, err := b.options(info.Value)
if err != nil {
wrap.JsonApiErr(http.StatusBadRequest, "error getting options", err)
return
}
// Use the existing snapshot service
dashboardsnapshots.CreateDashboardSnapshot(wrap, opts.Spec, b.service)
},
},
{
Path: prefix + "/delete/{deleteKey}",
Spec: &spec3.PathProps{
Summary: "an example at the root level",
Description: "longer description here?",
Delete: &spec3.Operation{
OperationProps: spec3.OperationProps{
Tags: tags,
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, b.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.",
})
},
},
},
}
// dev environment to export all snapshots to a blob store
if b.exporter != nil && false {
routes.Root = append(routes.Root, b.exporter.getAPIRouteHandler())
}
return routes
}
func (b *SnapshotsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
// TODO: this behavior must match the existing logic (it is currently more restrictive)
//
// https://github.com/grafana/grafana/blob/f63e43c113ac0cf8f78ed96ee2953874139bd2dc/pkg/middleware/auth.go#L203
// func SnapshotPublicModeOrSignedIn(cfg *setting.Cfg) web.Handler {
// return func(c *contextmodel.ReqContext) {
// if cfg.SnapshotPublicMode {
// return
// }
// if !c.IsSignedIn {
// notAuthorized(c)
// return
// }
// }
// }
return authorizer.AuthorizerFunc(
func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
// Everyone can view dashsnaps
if attr.GetVerb() == "get" && attr.GetResource() == dashboardsnapshot.DashboardSnapshotResourceInfo.GroupResource().Resource {
return authorizer.DecisionAllow, "", err
}
// Fallback to the default behaviors (namespace matches org)
return authorizer.DecisionNoOpinion, "", err
})
}
@@ -0,0 +1,147 @@
package dashboardsnapshot
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"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
)
var (
_ rest.Scoper = (*legacyStorage)(nil)
_ rest.SingularNameProvider = (*legacyStorage)(nil)
_ rest.Getter = (*legacyStorage)(nil)
_ rest.Lister = (*legacyStorage)(nil)
_ rest.Storage = (*legacyStorage)(nil)
_ rest.GracefulDeleter = (*legacyStorage)(nil)
)
type legacyStorage struct {
service dashboardsnapshots.Service
namespacer request.NamespaceMapper
tableConverter rest.TableConvertor
options sharingOptionsGetter
}
func (s *legacyStorage) New() runtime.Object {
return resourceInfo.NewFunc()
}
func (s *legacyStorage) Destroy() {}
func (s *legacyStorage) NamespaceScoped() bool {
return true // namespace == org
}
func (s *legacyStorage) GetSingularName() string {
return resourceInfo.GetSingularName()
}
func (s *legacyStorage) NewList() runtime.Object {
return resourceInfo.NewListFunc()
}
func (s *legacyStorage) checkEnabled(ns string) error {
opts, err := s.options(ns)
if err != nil {
return err
}
if !opts.Spec.SnapshotsEnabled {
return fmt.Errorf("snapshots not enabled")
}
return nil
}
func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
}
func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err == nil {
err = s.checkEnabled(info.Value)
}
if err != nil {
return nil, err
}
user, err := appcontext.User(ctx)
if err != nil {
return nil, err
}
limit := 5000
if options.Limit > 0 {
limit = int(options.Limit)
}
res, err := s.service.SearchDashboardSnapshots(ctx, &dashboardsnapshots.GetDashboardSnapshotsQuery{
OrgID: info.OrgID,
SignedInUser: user,
Limit: limit,
})
if err != nil {
return nil, err
}
list := &dashboardsnapshot.DashboardSnapshotList{}
for _, v := range res {
list.Items = append(list.Items, *convertDTOToSnapshot(v, s.namespacer))
}
return list, nil
}
func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err == nil {
err = s.checkEnabled(info.Value)
}
if err != nil {
return nil, err
}
v, err := s.service.GetDashboardSnapshot(ctx, &dashboardsnapshots.GetDashboardSnapshotQuery{
Key: name,
})
if err != nil || v == nil {
// if errors.Is(err, playlistsvc.ErrPlaylistNotFound) || err == nil {
// err = k8serrors.NewNotFound(s.SingularQualifiedResource, name)
// }
return nil, err
}
return convertSnapshotToK8sResource(v, s.namespacer), nil
}
// GracefulDeleter
func (s *legacyStorage) 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
}
@@ -0,0 +1,60 @@
package dashboardsnapshot
import (
"context"
"net/http"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
)
type subBodyREST struct {
service dashboardsnapshots.Service
namespacer request.NamespaceMapper
}
var _ = rest.Connecter(&subBodyREST{})
func (r *subBodyREST) New() runtime.Object {
return &dashboardsnapshot.FullDashboardSnapshot{}
}
func (r *subBodyREST) Destroy() {}
func (r *subBodyREST) ConnectMethods() []string {
return []string{"GET"}
}
func (r *subBodyREST) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, ""
}
func (r *subBodyREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
snap, err := r.service.GetDashboardSnapshot(ctx, &dashboardsnapshots.GetDashboardSnapshotQuery{
Key: name,
})
if err != nil {
responder.Error(err)
return
}
data, err := snap.Dashboard.Map()
if err != nil {
responder.Error(err)
return
}
r := convertSnapshotToK8sResource(snap, r.namespacer)
responder.Object(200, &dashboardsnapshot.FullDashboardSnapshot{
ObjectMeta: r.ObjectMeta,
Info: r.Spec,
Dashboard: common.Unstructured{Object: data},
})
}), nil
}
+2
View File
@@ -4,6 +4,7 @@ import (
"github.com/google/wire"
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/registry/apis/example"
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
@@ -26,6 +27,7 @@ var WireSet = wire.NewSet(
playlist.RegisterAPIService,
dashboard.RegisterAPIService,
example.RegisterAPIService,
dashboardsnapshot.RegisterAPIService,
featuretoggle.RegisterAPIService,
datasource.RegisterAPIService,
folders.RegisterAPIService,