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
-3
View File
@@ -31,7 +31,6 @@ package api
import (
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -49,8 +48,6 @@ import (
"github.com/grafana/grafana/pkg/services/user"
)
var plog = log.New("api")
// registerRoutes registers all API HTTP routes.
func (hs *HTTPServer) registerRoutes() {
reqNoAuth := middleware.NoAuth()
+17 -200
View File
@@ -1,32 +1,22 @@
package api
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/simplejson"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
var client = &http.Client{
Timeout: time.Second * 5,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
// swagger:route GET /snapshot/shared-options snapshots getSharingOptions
//
// Get snapshot sharing settings.
@@ -43,58 +33,6 @@ func (hs *HTTPServer) GetSharingOptions(c *contextmodel.ReqContext) {
})
}
type CreateExternalSnapshotResponse struct {
Key string `json:"key"`
DeleteKey string `json:"deleteKey"`
Url string `json:"url"`
DeleteUrl string `json:"deleteUrl"`
}
func createExternalDashboardSnapshot(cmd dashboardsnapshots.CreateDashboardSnapshotCommand, externalSnapshotUrl string) (*CreateExternalSnapshotResponse, error) {
var createSnapshotResponse CreateExternalSnapshotResponse
message := map[string]any{
"name": cmd.Name,
"expires": cmd.Expires,
"dashboard": cmd.Dashboard,
"key": cmd.Key,
"deleteKey": cmd.DeleteKey,
}
messageBytes, err := simplejson.NewFromAny(message).Encode()
if err != nil {
return nil, err
}
resp, err := client.Post(externalSnapshotUrl+"/api/snapshots", "application/json", bytes.NewBuffer(messageBytes))
if err != nil {
return nil, err
}
defer func() {
if err := resp.Body.Close(); err != nil {
plog.Warn("Failed to close response body", "err", err)
}
}()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("create external snapshot response status code %d", resp.StatusCode)
}
if err := json.NewDecoder(resp.Body).Decode(&createSnapshotResponse); err != nil {
return nil, err
}
return &createSnapshotResponse, nil
}
func createOriginalDashboardURL(cmd *dashboardsnapshots.CreateDashboardSnapshotCommand) (string, error) {
dashUID := cmd.Dashboard.Get("uid").MustString("")
if ok := util.IsValidShortUID(dashUID); !ok {
return "", fmt.Errorf("invalid dashboard UID")
}
return fmt.Sprintf("/d/%v", dashUID), nil
}
// swagger:route POST /snapshots snapshots createDashboardSnapshot
//
// When creating a snapshot using the API, you have to provide the full dashboard payload including the snapshot data. This endpoint is designed for the Grafana UI.
@@ -106,95 +44,13 @@ func createOriginalDashboardURL(cmd *dashboardsnapshots.CreateDashboardSnapshotC
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) CreateDashboardSnapshot(c *contextmodel.ReqContext) response.Response {
if !hs.Cfg.SnapshotEnabled {
c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil)
return nil
}
cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
if cmd.Name == "" {
cmd.Name = "Unnamed snapshot"
}
userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if err != nil {
return response.Error(http.StatusInternalServerError,
"Failed to create external snapshot", err)
}
var snapshotUrl string
cmd.ExternalURL = ""
cmd.OrgID = c.SignedInUser.GetOrgID()
cmd.UserID = userID
originalDashboardURL, err := createOriginalDashboardURL(&cmd)
if err != nil {
return response.Error(http.StatusInternalServerError, "Invalid app URL", err)
}
if cmd.External {
if !hs.Cfg.ExternalEnabled {
c.JsonApiErr(http.StatusForbidden, "External dashboard creation is disabled", nil)
return nil
}
resp, err := createExternalDashboardSnapshot(cmd, hs.Cfg.ExternalSnapshotUrl)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Failed to create external snapshot", err)
return nil
}
snapshotUrl = resp.Url
cmd.Key = resp.Key
cmd.DeleteKey = resp.DeleteKey
cmd.ExternalURL = resp.Url
cmd.ExternalDeleteURL = resp.DeleteUrl
cmd.Dashboard = simplejson.New()
metrics.MApiDashboardSnapshotExternal.Inc()
} else {
cmd.Dashboard.SetPath([]string{"snapshot", "originalUrl"}, originalDashboardURL)
if cmd.Key == "" {
var err error
cmd.Key, err = util.GetRandomString(32)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Could not generate random string", err)
return nil
}
}
if cmd.DeleteKey == "" {
var err error
cmd.DeleteKey, err = util.GetRandomString(32)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Could not generate random string", err)
return nil
}
}
snapshotUrl = setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key)
metrics.MApiDashboardSnapshotCreate.Inc()
}
result, err := hs.dashboardsnapshotsService.CreateDashboardSnapshot(c.Req.Context(), &cmd)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Failed to create snapshot", err)
return nil
}
c.JSON(http.StatusOK, util.DynMap{
"key": cmd.Key,
"deleteKey": cmd.DeleteKey,
"url": snapshotUrl,
"deleteUrl": setting.ToAbsUrl("api/snapshots-delete/" + cmd.DeleteKey),
"id": result.ID,
})
return nil
func (hs *HTTPServer) CreateDashboardSnapshot(c *contextmodel.ReqContext) {
dashboardsnapshots.CreateDashboardSnapshot(c, dashboardsnapshot.SnapshotSharingOptions{
SnapshotsEnabled: hs.Cfg.SnapshotEnabled,
ExternalEnabled: hs.Cfg.ExternalEnabled,
ExternalSnapshotName: hs.Cfg.ExternalSnapshotName,
ExternalSnapshotURL: hs.Cfg.ExternalSnapshotUrl,
}, hs.dashboardsnapshotsService)
}
// GET /api/snapshots/:key
@@ -247,38 +103,6 @@ func (hs *HTTPServer) GetDashboardSnapshot(c *contextmodel.ReqContext) response.
return response.JSON(http.StatusOK, dto).SetHeader("Cache-Control", "public, max-age=3600")
}
func deleteExternalDashboardSnapshot(externalUrl string) error {
resp, err := client.Get(externalUrl)
if err != nil {
return err
}
defer func() {
if err := resp.Body.Close(); err != nil {
plog.Warn("Failed to close response body", "err", err)
}
}()
if resp.StatusCode == 200 {
return nil
}
// Gracefully ignore "snapshot not found" errors as they could have already
// been removed either via the cleanup script or by request.
if resp.StatusCode == 500 {
var respJson map[string]any
if err := json.NewDecoder(resp.Body).Decode(&respJson); err != nil {
return err
}
if respJson["message"] == "Failed to get dashboard snapshot" {
return nil
}
}
return fmt.Errorf("unexpected response when deleting external snapshot, status code: %d", resp.StatusCode)
}
// swagger:route GET /snapshots-delete/{deleteKey} snapshots deleteDashboardSnapshotByDeleteKey
//
// Delete Snapshot by deleteKey.
@@ -302,28 +126,16 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *contextmodel.ReqCont
return response.Error(404, "Snapshot not found", nil)
}
query := &dashboardsnapshots.GetDashboardSnapshotQuery{DeleteKey: key}
queryResult, err := hs.dashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
err := dashboardsnapshots.DeleteWithKey(c.Req.Context(), key, hs.dashboardsnapshotsService)
if err != nil {
return response.Err(err)
}
if queryResult.External {
err := deleteExternalDashboardSnapshot(queryResult.ExternalDeleteURL)
if err != nil {
return response.Error(500, "Failed to delete external dashboard", err)
if errors.Is(err, dashboardsnapshots.ErrBaseNotFound) {
return response.Error(404, "Snapshot not found", err)
}
}
cmd := &dashboardsnapshots.DeleteDashboardSnapshotCommand{DeleteKey: queryResult.DeleteKey}
if err := hs.dashboardsnapshotsService.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil {
return response.Error(500, "Failed to delete dashboard snapshot", err)
}
return response.JSON(http.StatusOK, util.DynMap{
"message": "Snapshot deleted. It might take an hour before it's cleared from any CDN caches.",
"id": queryResult.ID,
})
}
@@ -357,8 +169,13 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *contextmodel.ReqContext) respon
return response.Error(http.StatusNotFound, "Failed to get dashboard snapshot", nil)
}
// TODO: enforce org ID same
// if queryResult.OrgID != c.OrgID {
// return response.Error(http.StatusUnauthorized, "OrgID mismatch", nil)
// }
if queryResult.External {
err := deleteExternalDashboardSnapshot(queryResult.ExternalDeleteURL)
err := dashboardsnapshots.DeleteExternalDashboardSnapshot(queryResult.ExternalDeleteURL)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to delete external dashboard", err)
}
+11 -10
View File
@@ -9,13 +9,14 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/web/webtest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
@@ -148,12 +149,11 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey
sc.fakeReqWithParams("GET", sc.url, map[string]string{"deleteKey": "12345"}).exec()
require.Equal(t, 200, sc.resp.Code)
require.Equal(t, 200, sc.resp.Code, "BODY: "+sc.resp.Body.String())
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
require.NoError(t, err)
assert.True(t, strings.HasPrefix(respJSON.Get("message").MustString(), "Snapshot deleted"))
assert.Equal(t, 1, respJSON.Get("id").MustInt())
assert.Equal(t, http.MethodGet, externalRequest.Method)
assert.Equal(t, ts.URL, fmt.Sprintf("http://%s", externalRequest.Host))
@@ -271,7 +271,7 @@ func TestGetDashboardSnapshotNotFound(t *testing.T) {
sc.handlerFunc = hs.DeleteDashboardSnapshot
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
assert.Equal(t, http.StatusNotFound, sc.resp.Code)
assert.Equal(t, http.StatusNotFound, sc.resp.Code, "BODY: "+sc.resp.Body.String())
}, sqlmock)
loggedInUserScenarioWithRole(t,
@@ -282,7 +282,7 @@ func TestGetDashboardSnapshotNotFound(t *testing.T) {
sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec()
assert.Equal(t, http.StatusNotFound, sc.resp.Code)
assert.Equal(t, http.StatusNotFound, sc.resp.Code, "BODY: "+sc.resp.Body.String())
}, sqlmock)
}
@@ -345,7 +345,7 @@ func TestGetDashboardSnapshotFailure(t *testing.T) {
sc.handlerFunc = hs.DeleteDashboardSnapshot
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
assert.Equal(t, http.StatusForbidden, sc.resp.Code)
assert.Equal(t, http.StatusForbidden, sc.resp.Code, "BODY: "+sc.resp.Body.String())
}, sqlmock)
loggedInUserScenarioWithRole(t,
@@ -356,7 +356,7 @@ func TestGetDashboardSnapshotFailure(t *testing.T) {
sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec()
assert.Equal(t, http.StatusInternalServerError, sc.resp.Code)
assert.Equal(t, http.StatusInternalServerError, sc.resp.Code, "BODY: "+sc.resp.Body.String())
}, sqlmock)
loggedInUserScenarioWithRole(t,
@@ -367,7 +367,7 @@ func TestGetDashboardSnapshotFailure(t *testing.T) {
sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec()
assert.Equal(t, http.StatusForbidden, sc.resp.Code)
assert.Equal(t, http.StatusForbidden, sc.resp.Code, "BODY: "+sc.resp.Body.String())
}, sqlmock)
}
@@ -391,6 +391,7 @@ func setUpSnapshotTest(t *testing.T, userId int64, deleteUrl string) dashboardsn
res := &dashboardsnapshots.DashboardSnapshot{
ID: 1,
OrgID: 1,
Key: "12345",
DeleteKey: "54321",
Dashboard: jsonModel,
@@ -0,0 +1,6 @@
// +k8s:deepcopy-gen=package
// +k8s:openapi-gen=true
// +k8s:defaulter-gen=TypeMeta
// +groupName=dashboardsnapshot.grafana.app
package v0alpha1
@@ -0,0 +1,25 @@
package v0alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
)
const (
GROUP = "dashboardsnapshot.grafana.app"
VERSION = "v0alpha1"
APIVERSION = GROUP + "/" + VERSION
)
var DashboardSnapshotResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"dashboardsnapshots", "dashboardsnapshot", "DashboardSnapshot",
func() runtime.Object { return &DashboardSnapshot{} },
func() runtime.Object { return &DashboardSnapshotList{} },
)
var (
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION}
)
@@ -0,0 +1,136 @@
package v0alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DashboardSnapshot struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`
// Snapshot summary info
Spec SnapshotInfo `json:"spec"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DashboardSnapshotList struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ListMeta `json:"metadata,omitempty"`
Items []DashboardSnapshot `json:"items,omitempty"`
}
type SnapshotInfo struct {
Title string `json:"title,omitempty"`
// Optionally auto-remove the snapshot at a future date
Expires int64 `json:"expires,omitempty"`
// When set to true, the snapshot exists in a remote server
External bool `json:"external,omitempty"`
// The external URL where the snapshot can be seen
ExternalURL string `json:"externalUrl,omitempty"`
// The URL that created the dashboard originally
OriginalUrl string `json:"originalUrl,omitempty"`
// Snapshot creation timestamp
Timestamp string `json:"timestamp,omitempty"`
}
// This is returned from the POST command
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DashboardSnapshotWithDeleteKey struct {
DashboardSnapshot `json:",inline"`
// The delete key is only returned when the item is created. It is not returned from a get request
DeleteKey string `json:"deleteKey,omitempty"`
}
// This is the snapshot returned from the subresource
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type FullDashboardSnapshot struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`
// Snapshot summary info
Info SnapshotInfo `json:"info"`
// The raw dashboard (unstructured for now)
Dashboard common.Unstructured `json:"dashboard"`
}
// Each tenant, may have different sharing options
// This is currently set using custom.ini, but multi-tenant support will need
// to be managed differently
type SnapshotSharingOptions struct {
SnapshotsEnabled bool `json:"snapshotEnabled"`
ExternalSnapshotURL string `json:"externalSnapshotURL,omitempty"`
ExternalSnapshotName string `json:"externalSnapshotName,omitempty"`
ExternalEnabled bool `json:"externalEnabled,omitempty"`
}
// These are the values expected to be sent from an end user
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DashboardCreateCommand struct {
metav1.TypeMeta `json:",inline"`
// Snapshot name
// required:false
Name string `json:"name"`
// The complete dashboard model.
// required:true
Dashboard *common.Unstructured `json:"dashboard" binding:"Required"`
// When the snapshot should expire in seconds in seconds. Default is never to expire.
// required:false
// default:0
Expires int64 `json:"expires"`
// these are passed when storing an external snapshot ref
// Save the snapshot on an external server rather than locally.
// required:false
// default: false
External bool `json:"external"`
}
// The create response
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DashboardCreateResponse struct {
metav1.TypeMeta `json:",inline"`
// The unique key
Key string `json:"key"`
// A unique key that will allow delete
DeleteKey string `json:"deleteKey"`
// Absolute URL to show the dashboard
URL string `json:"url"`
// URL that will delete the response
DeleteURL string `json:"deleteUrl"`
}
// Represents an options object that must be named for each namespace/team/user
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type SharingOptions struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// Show the options inline
Spec SnapshotSharingOptions `json:"spec"`
}
// Represents a list of namespaced options
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type SharingOptionsList struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ListMeta `json:"metadata,omitempty"`
Items []SharingOptions `json:"items,omitempty"`
}
@@ -0,0 +1,271 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by deepcopy-gen. DO NOT EDIT.
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardCreateCommand) DeepCopyInto(out *DashboardCreateCommand) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.Dashboard != nil {
in, out := &in.Dashboard, &out.Dashboard
*out = (*in).DeepCopy()
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardCreateCommand.
func (in *DashboardCreateCommand) DeepCopy() *DashboardCreateCommand {
if in == nil {
return nil
}
out := new(DashboardCreateCommand)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DashboardCreateCommand) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardCreateResponse) DeepCopyInto(out *DashboardCreateResponse) {
*out = *in
out.TypeMeta = in.TypeMeta
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardCreateResponse.
func (in *DashboardCreateResponse) DeepCopy() *DashboardCreateResponse {
if in == nil {
return nil
}
out := new(DashboardCreateResponse)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DashboardCreateResponse) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardSnapshot) DeepCopyInto(out *DashboardSnapshot) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSnapshot.
func (in *DashboardSnapshot) DeepCopy() *DashboardSnapshot {
if in == nil {
return nil
}
out := new(DashboardSnapshot)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DashboardSnapshot) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardSnapshotList) DeepCopyInto(out *DashboardSnapshotList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]DashboardSnapshot, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSnapshotList.
func (in *DashboardSnapshotList) DeepCopy() *DashboardSnapshotList {
if in == nil {
return nil
}
out := new(DashboardSnapshotList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DashboardSnapshotList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardSnapshotWithDeleteKey) DeepCopyInto(out *DashboardSnapshotWithDeleteKey) {
*out = *in
in.DashboardSnapshot.DeepCopyInto(&out.DashboardSnapshot)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSnapshotWithDeleteKey.
func (in *DashboardSnapshotWithDeleteKey) DeepCopy() *DashboardSnapshotWithDeleteKey {
if in == nil {
return nil
}
out := new(DashboardSnapshotWithDeleteKey)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DashboardSnapshotWithDeleteKey) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FullDashboardSnapshot) DeepCopyInto(out *FullDashboardSnapshot) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Info = in.Info
in.Dashboard.DeepCopyInto(&out.Dashboard)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FullDashboardSnapshot.
func (in *FullDashboardSnapshot) DeepCopy() *FullDashboardSnapshot {
if in == nil {
return nil
}
out := new(FullDashboardSnapshot)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *FullDashboardSnapshot) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SharingOptions) DeepCopyInto(out *SharingOptions) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SharingOptions.
func (in *SharingOptions) DeepCopy() *SharingOptions {
if in == nil {
return nil
}
out := new(SharingOptions)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *SharingOptions) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SharingOptionsList) DeepCopyInto(out *SharingOptionsList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]SharingOptions, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SharingOptionsList.
func (in *SharingOptionsList) DeepCopy() *SharingOptionsList {
if in == nil {
return nil
}
out := new(SharingOptionsList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *SharingOptionsList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SnapshotInfo) DeepCopyInto(out *SnapshotInfo) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotInfo.
func (in *SnapshotInfo) DeepCopy() *SnapshotInfo {
if in == nil {
return nil
}
out := new(SnapshotInfo)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SnapshotSharingOptions) DeepCopyInto(out *SnapshotSharingOptions) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotSharingOptions.
func (in *SnapshotSharingOptions) DeepCopy() *SnapshotSharingOptions {
if in == nil {
return nil
}
out := new(SnapshotSharingOptions)
in.DeepCopyInto(out)
return out
}
@@ -0,0 +1,19 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by defaulter-gen. DO NOT EDIT.
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterDefaults adds defaulters functions to the given scheme.
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
return nil
}
@@ -0,0 +1,521 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by openapi-gen. DO NOT EDIT.
// This file was autogenerated by openapi-gen. Do not edit it manually!
package v0alpha1
import (
common "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec"
)
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardCreateCommand": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateCommand(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardCreateResponse": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateResponse(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshot": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshot(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshotList": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshotList(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshotWithDeleteKey": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshotWithDeleteKey(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.FullDashboardSnapshot": schema_pkg_apis_dashboardsnapshot_v0alpha1_FullDashboardSnapshot(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SharingOptions": schema_pkg_apis_dashboardsnapshot_v0alpha1_SharingOptions(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SharingOptionsList": schema_pkg_apis_dashboardsnapshot_v0alpha1_SharingOptionsList(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo": schema_pkg_apis_dashboardsnapshot_v0alpha1_SnapshotInfo(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotSharingOptions": schema_pkg_apis_dashboardsnapshot_v0alpha1_SnapshotSharingOptions(ref),
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateCommand(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "These are the values expected to be sent from an end user",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"name": {
SchemaProps: spec.SchemaProps{
Description: "Snapshot name required:false",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"dashboard": {
SchemaProps: spec.SchemaProps{
Description: "The complete dashboard model. required:true",
Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"),
},
},
"expires": {
SchemaProps: spec.SchemaProps{
Description: "When the snapshot should expire in seconds in seconds. Default is never to expire. required:false default:0",
Default: 0,
Type: []string{"integer"},
Format: "int64",
},
},
"external": {
SchemaProps: spec.SchemaProps{
Description: "these are passed when storing an external snapshot ref Save the snapshot on an external server rather than locally. required:false default: false",
Default: false,
Type: []string{"boolean"},
Format: "",
},
},
},
Required: []string{"name", "dashboard", "expires", "external"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateResponse(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "The create response",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"key": {
SchemaProps: spec.SchemaProps{
Description: "The unique key",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"deleteKey": {
SchemaProps: spec.SchemaProps{
Description: "A unique key that will allow delete",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"url": {
SchemaProps: spec.SchemaProps{
Description: "Absolute URL to show the dashboard",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"deleteUrl": {
SchemaProps: spec.SchemaProps{
Description: "URL that will delete the response",
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"key", "deleteKey", "url", "deleteUrl"},
},
},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshot(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Description: "Snapshot summary info",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo"),
},
},
},
Required: []string{"spec"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshotList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshot"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshot", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshotWithDeleteKey(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "This is returned from the POST command",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Description: "Snapshot summary info",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo"),
},
},
"deleteKey": {
SchemaProps: spec.SchemaProps{
Description: "The delete key is only returned when the item is created. It is not returned from a get request",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"spec"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_FullDashboardSnapshot(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "This is the snapshot returned from the subresource",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"info": {
SchemaProps: spec.SchemaProps{
Description: "Snapshot summary info",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo"),
},
},
"dashboard": {
SchemaProps: spec.SchemaProps{
Description: "The raw dashboard (unstructured for now)",
Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"),
},
},
},
Required: []string{"info", "dashboard"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured", "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_SharingOptions(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Represents an options object that must be named for each namespace/team/user",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Description: "Show the options inline",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotSharingOptions"),
},
},
},
Required: []string{"spec"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotSharingOptions", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_SharingOptionsList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Represents a list of namespaced options",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SharingOptions"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SharingOptions", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_SnapshotInfo(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"title": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"expires": {
SchemaProps: spec.SchemaProps{
Description: "Optionally auto-remove the snapshot at a future date",
Type: []string{"integer"},
Format: "int64",
},
},
"external": {
SchemaProps: spec.SchemaProps{
Description: "When set to true, the snapshot exists in a remote server",
Type: []string{"boolean"},
Format: "",
},
},
"externalUrl": {
SchemaProps: spec.SchemaProps{
Description: "The external URL where the snapshot can be seen",
Type: []string{"string"},
Format: "",
},
},
"originalUrl": {
SchemaProps: spec.SchemaProps{
Description: "The URL that created the dashboard originally",
Type: []string{"string"},
Format: "",
},
},
"timestamp": {
SchemaProps: spec.SchemaProps{
Description: "Snapshot creation timestamp",
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_SnapshotSharingOptions(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Each tenant, may have different sharing options This is currently set using custom.ini, but multi-tenant support will need to be managed differently",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"snapshotEnabled": {
SchemaProps: spec.SchemaProps{
Default: false,
Type: []string{"boolean"},
Format: "",
},
},
"externalSnapshotURL": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"externalSnapshotName": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"externalEnabled": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
Format: "",
},
},
},
Required: []string{"snapshotEnabled"},
},
},
}
}
@@ -0,0 +1,3 @@
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1,DashboardCreateResponse,DeleteURL
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1,SnapshotInfo,ExternalURL
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1,SnapshotSharingOptions,SnapshotsEnabled
+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,
@@ -16,26 +16,36 @@ import (
type DashboardSnapshotStore struct {
store db.DB
log log.Logger
cfg *setting.Cfg
// deprecated behavior
skipDeleteExpired bool
}
// DashboardStore implements the Store interface
var _ dashboardsnapshots.Store = (*DashboardSnapshotStore)(nil)
func ProvideStore(db db.DB, cfg *setting.Cfg) *DashboardSnapshotStore {
return &DashboardSnapshotStore{store: db, log: log.New("dashboardsnapshot.store"), cfg: cfg}
// nolint:staticcheck
return NewStore(db, !cfg.SnapShotRemoveExpired)
}
func NewStore(db db.DB, skipDeleteExpired bool) *DashboardSnapshotStore {
log := log.New("dashboardsnapshot.store")
if skipDeleteExpired {
log.Warn("[Deprecated] The snapshot_remove_expired setting is outdated. Please remove from your config.")
}
return &DashboardSnapshotStore{store: db, skipDeleteExpired: skipDeleteExpired}
}
// DeleteExpiredSnapshots removes snapshots with old expiry dates.
// SnapShotRemoveExpired is deprecated and should be removed in the future.
// Snapshot expiry is decided by the user when they share the snapshot.
func (d *DashboardSnapshotStore) DeleteExpiredSnapshots(ctx context.Context, cmd *dashboardsnapshots.DeleteExpiredSnapshotsCommand) error {
if d.skipDeleteExpired {
d.log.Warn("[Deprecated] The snapshot_remove_expired setting is outdated. Please remove from your config.")
return nil
}
return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
if !d.cfg.SnapShotRemoveExpired {
d.log.Warn("[Deprecated] The snapshot_remove_expired setting is outdated. Please remove from your config.")
return nil
}
deleteExpiredSQL := "DELETE FROM dashboard_snapshot WHERE expires < ?"
expiredResponse, err := sess.Exec(deleteExpiredSQL, time.Now())
if err != nil {
@@ -8,6 +8,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
@@ -116,9 +118,11 @@ func TestIntegrationDashboardSnapshotDBAccess(t *testing.T) {
cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{
Key: "strangesnapshotwithuserid0",
DeleteKey: "adeletekey",
Dashboard: simplejson.NewFromAny(map[string]any{
"hello": "mupp",
}),
DashboardCreateCommand: dashboardsnapshot.DashboardCreateCommand{
Dashboard: &common.Unstructured{Object: map[string]any{
"hello": "mupp",
}},
},
UserID: 0,
OrgID: 1,
}
@@ -155,11 +159,9 @@ func TestIntegrationDeleteExpiredSnapshots(t *testing.T) {
t.Skip("skipping integration test")
}
sqlstore := db.InitTestDB(t)
dashStore := ProvideStore(sqlstore, setting.NewCfg())
dashStore := NewStore(sqlstore, false)
t.Run("Testing dashboard snapshots clean up", func(t *testing.T) {
dashStore.cfg.SnapShotRemoveExpired = true
nonExpiredSnapshot := createTestSnapshot(t, dashStore, "key1", 48000)
createTestSnapshot(t, dashStore, "key2", -1200)
createTestSnapshot(t, dashStore, "key3", -1200)
@@ -196,12 +198,14 @@ func createTestSnapshot(t *testing.T, dashStore *DashboardSnapshotStore, key str
cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{
Key: key,
DeleteKey: "delete" + key,
Dashboard: simplejson.NewFromAny(map[string]any{
"hello": "mupp",
}),
UserID: 1000,
OrgID: 1,
Expires: expires,
DashboardCreateCommand: dashboardsnapshot.DashboardCreateCommand{
Expires: expires,
Dashboard: &common.Unstructured{Object: map[string]any{
"hello": "mupp",
}},
},
UserID: 1000,
OrgID: 1,
}
result, err := dashStore.CreateDashboardSnapshot(context.Background(), &cmd)
require.NoError(t, err)
+12 -15
View File
@@ -3,6 +3,7 @@ package dashboardsnapshots
import (
"time"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/auth/identity"
)
@@ -47,28 +48,17 @@ type DashboardSnapshotDTO struct {
// swagger:model
type CreateDashboardSnapshotCommand struct {
// The complete dashboard model.
// required:true
Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
// Snapshot name
// required:false
Name string `json:"name"`
// When the snapshot should expire in seconds in seconds. Default is never to expire.
// required:false
// default:0
Expires int64 `json:"expires"`
// The "public" fields are defined in this struct while the private/SQL/response params are
// defied in the rest of this command
dashboardsnapshot.DashboardCreateCommand
// these are passed when storing an external snapshot ref
// Save the snapshot on an external server rather than locally.
// required:false
// default: false
External bool `json:"external"`
ExternalURL string `json:"-"`
ExternalDeleteURL string `json:"-"`
// Define the unique key. Required if `external` is `true`.
// required:false
Key string `json:"key"`
// Unique key used to delete the snapshot. It is different from the `key` so that only the creator can delete the snapshot. Required if `external` is `true`.
// required:false
DeleteKey string `json:"deleteKey"`
@@ -100,3 +90,10 @@ type GetDashboardSnapshotsQuery struct {
OrgID int64
SignedInUser identity.Requester
}
type CreateExternalSnapshotResponse struct {
Key string `json:"key"`
DeleteKey string `json:"deleteKey"`
Url string `json:"url"`
DeleteUrl string `json:"deleteUrl"`
}
+211
View File
@@ -1,7 +1,23 @@
package dashboardsnapshots
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
//go:generate mockery --name Service --structname MockService --inpackage --filename service_mock.go
@@ -12,3 +28,198 @@ type Service interface {
GetDashboardSnapshot(context.Context, *GetDashboardSnapshotQuery) (*DashboardSnapshot, error)
SearchDashboardSnapshots(context.Context, *GetDashboardSnapshotsQuery) (DashboardSnapshotsList, error)
}
var client = &http.Client{
Timeout: time.Second * 5,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
func CreateDashboardSnapshot(c *contextmodel.ReqContext, cfg dashboardsnapshot.SnapshotSharingOptions, svc Service) {
if !cfg.SnapshotsEnabled {
c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil)
return
}
cmd := CreateDashboardSnapshotCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
c.JsonApiErr(http.StatusBadRequest, "bad request data", err)
return
}
if cmd.Name == "" {
cmd.Name = "Unnamed snapshot"
}
userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if err != nil {
c.JsonApiErr(http.StatusInternalServerError,
"Failed to create external snapshot", err)
return
}
var snapshotUrl string
cmd.ExternalURL = ""
cmd.OrgID = c.SignedInUser.GetOrgID()
cmd.UserID = userID
originalDashboardURL, err := createOriginalDashboardURL(&cmd)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Invalid app URL", err)
return
}
if cmd.External {
if !cfg.ExternalEnabled {
c.JsonApiErr(http.StatusForbidden, "External dashboard creation is disabled", nil)
return
}
resp, err := createExternalDashboardSnapshot(cmd, cfg.ExternalSnapshotURL)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Failed to create external snapshot", err)
return
}
snapshotUrl = resp.Url
cmd.Key = resp.Key
cmd.DeleteKey = resp.DeleteKey
cmd.ExternalURL = resp.Url
cmd.ExternalDeleteURL = resp.DeleteUrl
cmd.Dashboard = &common.Unstructured{}
metrics.MApiDashboardSnapshotExternal.Inc()
} else {
cmd.Dashboard.SetNestedField(originalDashboardURL, "snapshot", "originalUrl")
if cmd.Key == "" {
var err error
cmd.Key, err = util.GetRandomString(32)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Could not generate random string", err)
return
}
}
if cmd.DeleteKey == "" {
var err error
cmd.DeleteKey, err = util.GetRandomString(32)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Could not generate random string", err)
return
}
}
snapshotUrl = setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key)
metrics.MApiDashboardSnapshotCreate.Inc()
}
result, err := svc.CreateDashboardSnapshot(c.Req.Context(), &cmd)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Failed to create snapshot", err)
return
}
c.JSON(http.StatusOK, dashboardsnapshot.DashboardCreateResponse{
Key: result.Key,
DeleteKey: result.DeleteKey,
URL: snapshotUrl,
DeleteURL: setting.ToAbsUrl("api/snapshots-delete/" + result.DeleteKey),
})
}
var plog = log.New("external-snapshot")
func DeleteExternalDashboardSnapshot(externalUrl string) error {
resp, err := client.Get(externalUrl)
if err != nil {
return err
}
defer func() {
if err := resp.Body.Close(); err != nil {
plog.Warn("Failed to close response body", "err", err)
}
}()
if resp.StatusCode == 200 {
return nil
}
// Gracefully ignore "snapshot not found" errors as they could have already
// been removed either via the cleanup script or by request.
if resp.StatusCode == 500 {
var respJson map[string]any
if err := json.NewDecoder(resp.Body).Decode(&respJson); err != nil {
return err
}
if respJson["message"] == "Failed to get dashboard snapshot" {
return nil
}
}
return fmt.Errorf("unexpected response when deleting external snapshot, status code: %d", resp.StatusCode)
}
func createExternalDashboardSnapshot(cmd CreateDashboardSnapshotCommand, externalSnapshotUrl string) (*CreateExternalSnapshotResponse, error) {
var createSnapshotResponse CreateExternalSnapshotResponse
message := map[string]any{
"name": cmd.Name,
"expires": cmd.Expires,
"dashboard": cmd.Dashboard,
"key": cmd.Key,
"deleteKey": cmd.DeleteKey,
}
messageBytes, err := simplejson.NewFromAny(message).Encode()
if err != nil {
return nil, err
}
resp, err := client.Post(externalSnapshotUrl+"/api/snapshots", "application/json", bytes.NewBuffer(messageBytes))
if err != nil {
return nil, err
}
defer func() {
if err := resp.Body.Close(); err != nil {
plog.Warn("Failed to close response body", "err", err)
}
}()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("create external snapshot response status code %d", resp.StatusCode)
}
if err := json.NewDecoder(resp.Body).Decode(&createSnapshotResponse); err != nil {
return nil, err
}
return &createSnapshotResponse, nil
}
func createOriginalDashboardURL(cmd *CreateDashboardSnapshotCommand) (string, error) {
dashUID := cmd.Dashboard.GetNestedString("uid")
if ok := util.IsValidShortUID(dashUID); !ok {
return "", fmt.Errorf("invalid dashboard UID")
}
return fmt.Sprintf("/d/%v", dashUID), nil
}
func DeleteWithKey(ctx context.Context, key string, svc Service) error {
query := &GetDashboardSnapshotQuery{DeleteKey: key}
queryResult, err := svc.GetDashboardSnapshot(ctx, query)
if err != nil {
return err
}
if queryResult.External {
err := DeleteExternalDashboardSnapshot(queryResult.ExternalDeleteURL)
if err != nil {
return err
}
}
cmd := &DeleteDashboardSnapshotCommand{DeleteKey: queryResult.DeleteKey}
return svc.DeleteDashboardSnapshot(ctx, cmd)
}
@@ -26,7 +26,7 @@ func ProvideService(store dashboardsnapshots.Store, secretsService secrets.Servi
}
func (s *ServiceImpl) CreateDashboardSnapshot(ctx context.Context, cmd *dashboardsnapshots.CreateDashboardSnapshotCommand) (*dashboardsnapshots.DashboardSnapshot, error) {
marshalledData, err := cmd.Dashboard.Encode()
marshalledData, err := cmd.Dashboard.MarshalJSON()
if err != nil {
return nil, err
}
@@ -2,11 +2,13 @@ package service
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
dashsnapdb "github.com/grafana/grafana/pkg/services/dashboardsnapshots/database"
@@ -30,8 +32,9 @@ func TestDashboardSnapshotsService(t *testing.T) {
dashboardKey := "12345"
dashboard := &common.Unstructured{}
rawDashboard := []byte(`{"id":123}`)
dashboard, err := simplejson.NewJson(rawDashboard)
err := json.Unmarshal(rawDashboard, dashboard)
require.NoError(t, err)
t.Run("create dashboard snapshot should encrypt the dashboard", func(t *testing.T) {
@@ -40,7 +43,9 @@ func TestDashboardSnapshotsService(t *testing.T) {
cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{
Key: dashboardKey,
DeleteKey: dashboardKey,
Dashboard: dashboard,
DashboardCreateCommand: dashboardsnapshot.DashboardCreateCommand{
Dashboard: dashboard,
},
}
result, err := s.CreateDashboardSnapshot(ctx, &cmd)
+6 -4
View File
@@ -359,12 +359,14 @@ type Cfg struct {
SqlDatasourceMaxConnLifetimeDefault int
// Snapshots
SnapshotEnabled bool
ExternalSnapshotUrl string
ExternalSnapshotName string
ExternalEnabled bool
SnapshotEnabled bool
ExternalSnapshotUrl string
ExternalSnapshotName string
ExternalEnabled bool
// Deprecated: setting this to false adds deprecation warnings at runtime
SnapShotRemoveExpired bool
// Only used in https://snapshots.raintank.io/
SnapshotPublicMode bool
ErrTemplateName string
@@ -0,0 +1,80 @@
package dashboardsnapshots
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
)
func TestDashboardSnapshots(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
AppModeProduction: false, // required for experimental apis
DisableAnonymous: true,
EnableFeatureToggles: []string{
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // required to register dashboardsnapshot.grafana.app
},
})
t.Run("Check discovery client", func(t *testing.T) {
disco := helper.GetGroupVersionInfoJSON("dashboardsnapshot.grafana.app")
// fmt.Printf("%s", disco)
require.JSONEq(t, `[
{
"freshness": "Current",
"resources": [
{
"resource": "dashboardsnapshot",
"responseKind": {
"group": "",
"kind": "DashboardSnapshot",
"version": ""
},
"scope": "Namespaced",
"singularResource": "dashsnap",
"subresources": [
{
"responseKind": {
"group": "",
"kind": "FullDashboardSnapshot",
"version": ""
},
"subresource": "body",
"verbs": [
"get"
]
}
],
"verbs": [
"delete",
"get",
"list"
]
},
{
"resource": "options",
"responseKind": {
"group": "",
"kind": "SharingOptions",
"version": ""
},
"scope": "Namespaced",
"singularResource": "options",
"verbs": [
"get",
"list"
]
}
],
"version": "v0alpha1"
}
]`, disco)
})
}