mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s: Add dashboard service (requires dev mode) (#78565)
This commit is contained in:
parent
be12d3919f
commit
2c09f969f1
@ -14,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/apierrors"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/components/dashdiffs"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
@ -141,7 +142,7 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
|
||||
creator = hs.getUserLogin(c.Req.Context(), dash.CreatedBy)
|
||||
}
|
||||
|
||||
annotationPermissions := &dtos.AnnotationPermission{}
|
||||
annotationPermissions := &dashboardsV0.AnnotationPermission{}
|
||||
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Dashboard, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID))
|
||||
} else {
|
||||
@ -223,7 +224,7 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
|
||||
return response.JSON(http.StatusOK, dto)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getAnnotationPermissionsByScope(c *contextmodel.ReqContext, actions *dtos.AnnotationActions, scope string) {
|
||||
func (hs *HTTPServer) getAnnotationPermissionsByScope(c *contextmodel.ReqContext, actions *dashboardsV0.AnnotationActions, scope string) {
|
||||
var err error
|
||||
|
||||
evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, scope)
|
||||
|
@ -3,6 +3,7 @@ package dtos
|
||||
import (
|
||||
"time"
|
||||
|
||||
dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
@ -26,25 +27,15 @@ type DashboardMeta struct {
|
||||
HasACL bool `json:"hasAcl" xorm:"has_acl"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
// Deprecated: use FolderUID instead
|
||||
FolderId int64 `json:"folderId"`
|
||||
FolderUid string `json:"folderUid"`
|
||||
FolderTitle string `json:"folderTitle"`
|
||||
FolderUrl string `json:"folderUrl"`
|
||||
Provisioned bool `json:"provisioned"`
|
||||
ProvisionedExternalId string `json:"provisionedExternalId"`
|
||||
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
|
||||
PublicDashboardUID string `json:"publicDashboardUid,omitempty"`
|
||||
PublicDashboardEnabled bool `json:"publicDashboardEnabled,omitempty"`
|
||||
}
|
||||
type AnnotationPermission struct {
|
||||
Dashboard AnnotationActions `json:"dashboard"`
|
||||
Organization AnnotationActions `json:"organization"`
|
||||
}
|
||||
|
||||
type AnnotationActions struct {
|
||||
CanAdd bool `json:"canAdd"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanDelete bool `json:"canDelete"`
|
||||
FolderId int64 `json:"folderId"`
|
||||
FolderUid string `json:"folderUid"`
|
||||
FolderTitle string `json:"folderTitle"`
|
||||
FolderUrl string `json:"folderUrl"`
|
||||
Provisioned bool `json:"provisioned"`
|
||||
ProvisionedExternalId string `json:"provisionedExternalId"`
|
||||
AnnotationsPermissions *dashboardsV0.AnnotationPermission `json:"annotationsPermissions"`
|
||||
PublicDashboardUID string `json:"publicDashboardUid,omitempty"`
|
||||
PublicDashboardEnabled bool `json:"publicDashboardEnabled,omitempty"`
|
||||
}
|
||||
|
||||
type DashboardFullWithMeta struct {
|
||||
|
5
pkg/apis/dashboard/v0alpha1/doc.go
Normal file
5
pkg/apis/dashboard/v0alpha1/doc.go
Normal file
@ -0,0 +1,5 @@
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:openapi-gen=true
|
||||
// +groupName=dashboard.grafana.app
|
||||
|
||||
package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
124
pkg/apis/dashboard/v0alpha1/types.go
Normal file
124
pkg/apis/dashboard/v0alpha1/types.go
Normal file
@ -0,0 +1,124 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis"
|
||||
)
|
||||
|
||||
const (
|
||||
GROUP = "dashboard.grafana.app"
|
||||
VERSION = "v0alpha1"
|
||||
APIVERSION = GROUP + "/" + VERSION
|
||||
)
|
||||
|
||||
var DashboardResourceInfo = apis.NewResourceInfo(GROUP, VERSION,
|
||||
"dashboards", "dashboard", "Dashboard",
|
||||
func() runtime.Object { return &Dashboard{} },
|
||||
func() runtime.Object { return &DashboardList{} },
|
||||
)
|
||||
|
||||
var DashboardSummaryResourceInfo = apis.NewResourceInfo(GROUP, VERSION,
|
||||
"summary", "summary", "DashboardSummary",
|
||||
func() runtime.Object { return &DashboardSummary{} },
|
||||
func() runtime.Object { return &DashboardSummaryList{} },
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type Dashboard struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// Standard object's metadata
|
||||
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
|
||||
// +optional
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// The dashboard body (unstructured for now)
|
||||
Spec Unstructured `json:"spec"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type DashboardList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// +optional
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []Dashboard `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type DashboardSummary struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// The dashboard body
|
||||
Spec DashboardSummarySpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
type DashboardSummarySpec struct {
|
||||
Title string `json:"title"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type DashboardSummaryList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// +optional
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []DashboardSummary `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type DashboardVersionsInfo struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// +optional
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []DashboardVersionInfo `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
type DashboardVersionInfo struct {
|
||||
Version int `json:"version"`
|
||||
ParentVersion int `json:"parentVersion,omitempty"`
|
||||
Created int64 `json:"created"`
|
||||
Message string `json:"message,omitempty"`
|
||||
CreatedBy string `json:"createdBy,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:conversion-gen:explicit-from=net/url.Values
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type VersionsQueryOptions struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// Path is the URL path
|
||||
// +optional
|
||||
Path string `json:"path,omitempty"`
|
||||
|
||||
// +optional
|
||||
Version int64 `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// Information about how the requesting user can use a given dashboard
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type DashboardAccessInfo struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
CanSave bool `json:"canSave"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanAdmin bool `json:"canAdmin"`
|
||||
CanStar bool `json:"canStar"`
|
||||
CanDelete bool `json:"canDelete"`
|
||||
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
|
||||
}
|
||||
|
||||
type AnnotationPermission struct {
|
||||
Dashboard AnnotationActions `json:"dashboard"`
|
||||
Organization AnnotationActions `json:"organization"`
|
||||
}
|
||||
|
||||
type AnnotationActions struct {
|
||||
CanAdd bool `json:"canAdd"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanDelete bool `json:"canDelete"`
|
||||
}
|
100
pkg/apis/dashboard/v0alpha1/unstructured.go
Normal file
100
pkg/apis/dashboard/v0alpha1/unstructured.go
Normal file
@ -0,0 +1,100 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// Unstructured allows objects that do not have Golang structs registered to be manipulated
|
||||
// generically.
|
||||
type Unstructured struct {
|
||||
// Object is a JSON compatible map with string, float, int, bool, []interface{}, or
|
||||
// map[string]interface{}
|
||||
// children.
|
||||
Object map[string]interface{}
|
||||
}
|
||||
|
||||
func (u *Unstructured) UnstructuredContent() map[string]interface{} {
|
||||
if u.Object == nil {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
return u.Object
|
||||
}
|
||||
|
||||
func (u *Unstructured) SetUnstructuredContent(content map[string]interface{}) {
|
||||
u.Object = content
|
||||
}
|
||||
|
||||
// MarshalJSON ensures that the unstructured object produces proper
|
||||
// JSON when passed to Go's standard JSON library.
|
||||
func (u *Unstructured) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(u.Object)
|
||||
}
|
||||
|
||||
// UnmarshalJSON ensures that the unstructured object properly decodes
|
||||
// JSON when passed to Go's standard JSON library.
|
||||
func (u *Unstructured) UnmarshalJSON(b []byte) error {
|
||||
return json.Unmarshal(b, &u.Object)
|
||||
}
|
||||
|
||||
func (u *Unstructured) DeepCopy() *Unstructured {
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Unstructured)
|
||||
*out = *u
|
||||
out.Object = runtime.DeepCopyJSON(u.Object)
|
||||
return out
|
||||
}
|
||||
|
||||
func (u *Unstructured) DeepCopyInto(out *Unstructured) {
|
||||
clone := u.DeepCopy()
|
||||
*out = *clone
|
||||
}
|
||||
|
||||
func (u *Unstructured) Set(field string, value interface{}) {
|
||||
if u.Object == nil {
|
||||
u.Object = make(map[string]interface{})
|
||||
}
|
||||
_ = unstructured.SetNestedField(u.Object, value, field)
|
||||
}
|
||||
|
||||
func (u *Unstructured) Remove(fields ...string) {
|
||||
if u.Object == nil {
|
||||
u.Object = make(map[string]interface{})
|
||||
}
|
||||
unstructured.RemoveNestedField(u.Object, fields...)
|
||||
}
|
||||
|
||||
func (u *Unstructured) SetNestedField(value interface{}, fields ...string) {
|
||||
if u.Object == nil {
|
||||
u.Object = make(map[string]interface{})
|
||||
}
|
||||
_ = unstructured.SetNestedField(u.Object, value, fields...)
|
||||
}
|
||||
|
||||
func (u *Unstructured) GetNestedString(fields ...string) string {
|
||||
val, found, err := unstructured.NestedString(u.Object, fields...)
|
||||
if !found || err != nil {
|
||||
return ""
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (u *Unstructured) GetNestedStringSlice(fields ...string) []string {
|
||||
val, found, err := unstructured.NestedStringSlice(u.Object, fields...)
|
||||
if !found || err != nil {
|
||||
return nil
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (u *Unstructured) GetNestedInt64(fields ...string) int64 {
|
||||
val, found, err := unstructured.NestedInt64(u.Object, fields...)
|
||||
if !found || err != nil {
|
||||
return 0
|
||||
}
|
||||
return val
|
||||
}
|
289
pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go
Normal file
289
pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go
Normal file
@ -0,0 +1,289 @@
|
||||
//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 *AnnotationActions) DeepCopyInto(out *AnnotationActions) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnnotationActions.
|
||||
func (in *AnnotationActions) DeepCopy() *AnnotationActions {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(AnnotationActions)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AnnotationPermission) DeepCopyInto(out *AnnotationPermission) {
|
||||
*out = *in
|
||||
out.Dashboard = in.Dashboard
|
||||
out.Organization = in.Organization
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnnotationPermission.
|
||||
func (in *AnnotationPermission) DeepCopy() *AnnotationPermission {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(AnnotationPermission)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Dashboard) DeepCopyInto(out *Dashboard) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dashboard.
|
||||
func (in *Dashboard) DeepCopy() *Dashboard {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Dashboard)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Dashboard) 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 *DashboardAccessInfo) DeepCopyInto(out *DashboardAccessInfo) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.AnnotationsPermissions != nil {
|
||||
in, out := &in.AnnotationsPermissions, &out.AnnotationsPermissions
|
||||
*out = new(AnnotationPermission)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardAccessInfo.
|
||||
func (in *DashboardAccessInfo) DeepCopy() *DashboardAccessInfo {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DashboardAccessInfo)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *DashboardAccessInfo) 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 *DashboardList) DeepCopyInto(out *DashboardList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Dashboard, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardList.
|
||||
func (in *DashboardList) DeepCopy() *DashboardList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DashboardList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *DashboardList) 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 *DashboardSummary) DeepCopyInto(out *DashboardSummary) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSummary.
|
||||
func (in *DashboardSummary) DeepCopy() *DashboardSummary {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DashboardSummary)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *DashboardSummary) 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 *DashboardSummaryList) DeepCopyInto(out *DashboardSummaryList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]DashboardSummary, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSummaryList.
|
||||
func (in *DashboardSummaryList) DeepCopy() *DashboardSummaryList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DashboardSummaryList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *DashboardSummaryList) 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 *DashboardSummarySpec) DeepCopyInto(out *DashboardSummarySpec) {
|
||||
*out = *in
|
||||
if in.Tags != nil {
|
||||
in, out := &in.Tags, &out.Tags
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSummarySpec.
|
||||
func (in *DashboardSummarySpec) DeepCopy() *DashboardSummarySpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DashboardSummarySpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DashboardVersionInfo) DeepCopyInto(out *DashboardVersionInfo) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardVersionInfo.
|
||||
func (in *DashboardVersionInfo) DeepCopy() *DashboardVersionInfo {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DashboardVersionInfo)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DashboardVersionsInfo) DeepCopyInto(out *DashboardVersionsInfo) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]DashboardVersionInfo, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardVersionsInfo.
|
||||
func (in *DashboardVersionsInfo) DeepCopy() *DashboardVersionsInfo {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DashboardVersionsInfo)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *DashboardVersionsInfo) 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 *VersionsQueryOptions) DeepCopyInto(out *VersionsQueryOptions) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionsQueryOptions.
|
||||
func (in *VersionsQueryOptions) DeepCopy() *VersionsQueryOptions {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VersionsQueryOptions)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *VersionsQueryOptions) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
19
pkg/apis/dashboard/v0alpha1/zz_generated.defaults.go
Normal file
19
pkg/apis/dashboard/v0alpha1/zz_generated.defaults.go
Normal file
@ -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
|
||||
}
|
3012
pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go
Normal file
3012
pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -61,9 +61,15 @@ func User(ctx context.Context) (*user.SignedInUser, error) {
|
||||
IsGrafanaAdmin: true,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
orgId: {
|
||||
"*": {"*"},
|
||||
dashboards.ActionFoldersCreate: {"*"}, // all resources, all scopes
|
||||
dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, // access to read all folders
|
||||
"*": {"*"}, // all resources, all scopes
|
||||
|
||||
// Dashboards do not support wildcard action
|
||||
dashboards.ActionDashboardsRead: {"*"},
|
||||
dashboards.ActionDashboardsCreate: {"*"},
|
||||
dashboards.ActionDashboardsWrite: {"*"},
|
||||
dashboards.ActionDashboardsDelete: {"*"},
|
||||
dashboards.ActionFoldersCreate: {"*"},
|
||||
dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, // access to read all folders
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/example"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/folders"
|
||||
@ -19,6 +20,7 @@ type Service struct{}
|
||||
// ProvideRegistryServiceSink is an entry point for each service that will force initialization
|
||||
// and give each builder the chance to register itself with the main server
|
||||
func ProvideRegistryServiceSink(
|
||||
_ *dashboard.DashboardsAPIBuilder,
|
||||
_ *playlist.PlaylistAPIBuilder,
|
||||
_ *example.TestingAPIBuilder,
|
||||
_ *datasource.DataSourceAPIBuilder,
|
||||
|
422
pkg/registry/apis/dashboard/access/sql_dashboards.go
Normal file
422
pkg/registry/apis/dashboard/access/sql_dashboards.go
Normal file
@ -0,0 +1,422 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/appcontext"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/kinds"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/utils"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/session"
|
||||
)
|
||||
|
||||
var (
|
||||
_ DashboardAccess = (*dashboardSqlAccess)(nil)
|
||||
)
|
||||
|
||||
type dashboardRow struct {
|
||||
// Dashboard resource
|
||||
Dash *dashboardsV0.Dashboard
|
||||
|
||||
// Title -- this may come from saved metadata rather than the body
|
||||
Title string
|
||||
|
||||
// The folder UID (needed for access control checks)
|
||||
FolderUID string
|
||||
|
||||
// Needed for fast summary access
|
||||
Tags []string
|
||||
|
||||
// Size (in bytes) of the dashboard payload
|
||||
Bytes int
|
||||
|
||||
// The token we can use that will start a new connection that includes
|
||||
// this same dashboard
|
||||
token *continueToken
|
||||
}
|
||||
|
||||
type dashboardSqlAccess struct {
|
||||
sql db.DB
|
||||
sess *session.SessionDB
|
||||
namespacer request.NamespaceMapper
|
||||
dashStore dashboards.Store
|
||||
provisioning provisioning.ProvisioningService
|
||||
}
|
||||
|
||||
func NewDashboardAccess(sql db.DB, namespacer request.NamespaceMapper, dashStore dashboards.Store, provisioning provisioning.ProvisioningService) DashboardAccess {
|
||||
return &dashboardSqlAccess{
|
||||
sql: sql,
|
||||
sess: sql.GetSqlxSession(),
|
||||
namespacer: namespacer,
|
||||
dashStore: dashStore,
|
||||
provisioning: provisioning,
|
||||
}
|
||||
}
|
||||
|
||||
const selector = `SELECT
|
||||
dashboard.org_id, dashboard.id,
|
||||
dashboard.uid,slug,
|
||||
dashboard.folder_uid,
|
||||
dashboard.created,dashboard.created_by,CreatedUSER.login,
|
||||
dashboard.updated,dashboard.updated_by,UpdatedUSER.login,
|
||||
plugin_id,
|
||||
dashboard_provisioning.name as origin_name,
|
||||
dashboard_provisioning.external_id as origin_path,
|
||||
dashboard_provisioning.check_sum as origin_key,
|
||||
dashboard_provisioning.updated as origin_ts,
|
||||
dashboard.version,
|
||||
title,
|
||||
dashboard.data
|
||||
FROM dashboard
|
||||
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
|
||||
LEFT OUTER JOIN user AS CreatedUSER ON dashboard.created_by = CreatedUSER.id
|
||||
LEFT OUTER JOIN user AS UpdatedUSER ON dashboard.created_by = UpdatedUSER.id
|
||||
WHERE is_folder = false`
|
||||
|
||||
// GetDashboards implements DashboardAccess.
|
||||
func (a *dashboardSqlAccess) GetDashboards(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardList, error) {
|
||||
token, err := readContinueToken(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
limit := query.Limit
|
||||
if limit < 1 {
|
||||
limit = 15 //
|
||||
}
|
||||
|
||||
rows, err := a.doQuery(ctx, selector+`
|
||||
AND dashboard.org_id=$1
|
||||
AND dashboard.id>=$2
|
||||
ORDER BY dashboard.id asc
|
||||
LIMIT $3
|
||||
`, query.OrgID, token.id, (limit + 2))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
totalSize := 0
|
||||
list := &dashboardsV0.DashboardList{}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
row, err := rows.Next()
|
||||
if err != nil || row == nil {
|
||||
return list, err
|
||||
}
|
||||
|
||||
totalSize += row.Bytes
|
||||
if len(list.Items) > 0 && (totalSize > query.MaxBytes || len(list.Items) >= limit) {
|
||||
row.token.folder = query.FolderUID
|
||||
list.Continue = row.token.String() // will skip this one but start here next time
|
||||
return list, err
|
||||
}
|
||||
list.Items = append(list.Items, *row.Dash)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *dashboardSqlAccess) GetDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, error) {
|
||||
r, err := a.GetDashboards(ctx, &DashboardQuery{
|
||||
OrgID: orgId,
|
||||
UID: uid,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(r.Items) > 0 {
|
||||
return &r.Items[0], nil
|
||||
}
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
// GetDashboards implements DashboardAccess.
|
||||
func (a *dashboardSqlAccess) GetDashboardSummaries(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardSummaryList, error) {
|
||||
token, err := readContinueToken(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limit := query.Limit
|
||||
if limit < 1 {
|
||||
limit = 15 //
|
||||
}
|
||||
rows, err := a.doQuery(ctx, selector+`
|
||||
AND dashboard.org_id=$1
|
||||
AND dashboard.id>=$2
|
||||
ORDER BY dashboard.id asc
|
||||
LIMIT $3
|
||||
`, query.OrgID, token.id, (limit + 2))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
totalSize := 0
|
||||
list := &dashboardsV0.DashboardSummaryList{}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
row, err := rows.Next()
|
||||
if err != nil || row == nil {
|
||||
return list, err
|
||||
}
|
||||
|
||||
totalSize += row.Bytes
|
||||
if len(list.Items) > 0 && (totalSize > query.MaxBytes || len(list.Items) >= limit) {
|
||||
row.token.folder = query.FolderUID
|
||||
list.Continue = row.token.String() // will skip this one but start here next time
|
||||
return list, err
|
||||
}
|
||||
list.Items = append(list.Items, toSummary(row))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *dashboardSqlAccess) GetDashboardSummary(ctx context.Context, orgId int64, uid string) (*dashboardsV0.DashboardSummary, error) {
|
||||
r, err := a.GetDashboardSummaries(ctx, &DashboardQuery{
|
||||
OrgID: orgId,
|
||||
UID: uid,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(r.Items) > 0 {
|
||||
return &r.Items[0], nil
|
||||
}
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
func toSummary(row *dashboardRow) dashboardsV0.DashboardSummary {
|
||||
return dashboardsV0.DashboardSummary{
|
||||
ObjectMeta: row.Dash.ObjectMeta,
|
||||
Spec: dashboardsV0.DashboardSummarySpec{
|
||||
Title: row.Title,
|
||||
Tags: row.Tags,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *dashboardSqlAccess) doQuery(ctx context.Context, query string, args ...any) (*rowsWrapper, error) {
|
||||
user, err := appcontext.User(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := a.sess.Query(ctx, query, args...)
|
||||
return &rowsWrapper{
|
||||
rows: rows,
|
||||
a: a,
|
||||
// This looks up rules from the permissions on a user
|
||||
canReadDashboard: accesscontrol.Checker(user, dashboards.ActionDashboardsRead),
|
||||
}, err
|
||||
}
|
||||
|
||||
type rowsWrapper struct {
|
||||
a *dashboardSqlAccess
|
||||
rows *sql.Rows
|
||||
idx int
|
||||
total int64
|
||||
|
||||
canReadDashboard func(scopes ...string) bool
|
||||
}
|
||||
|
||||
func (r *rowsWrapper) Close() error {
|
||||
return r.rows.Close()
|
||||
}
|
||||
|
||||
func (r *rowsWrapper) Next() (*dashboardRow, error) {
|
||||
// breaks after first readable value
|
||||
for r.rows.Next() {
|
||||
r.idx++
|
||||
d, err := r.a.scanRow(r.rows)
|
||||
if d != nil {
|
||||
// Access control checker
|
||||
scopes := []string{dashboards.ScopeDashboardsProvider.GetResourceScopeUID(d.Dash.Name)}
|
||||
if d.FolderUID != "" { // Copied from searchV2... not sure the logic is right
|
||||
scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(d.FolderUID))
|
||||
}
|
||||
if !r.canReadDashboard(scopes...) {
|
||||
continue
|
||||
}
|
||||
d.token.size = r.total // size before next!
|
||||
r.total += int64(d.Bytes)
|
||||
}
|
||||
|
||||
// returns the first folder it can
|
||||
return d, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
|
||||
dash := &dashboardsV0.Dashboard{
|
||||
TypeMeta: dashboardsV0.DashboardResourceInfo.TypeMeta(),
|
||||
ObjectMeta: v1.ObjectMeta{Annotations: make(map[string]string)},
|
||||
}
|
||||
row := &dashboardRow{Dash: dash}
|
||||
|
||||
var dashboard_id int64
|
||||
var orgId int64
|
||||
var slug string
|
||||
var folder_uid sql.NullString
|
||||
var updated time.Time
|
||||
var updatedByID int64
|
||||
var updatedByName sql.NullString
|
||||
|
||||
var created time.Time
|
||||
var createdByID int64
|
||||
var createdByName sql.NullString
|
||||
|
||||
var plugin_id string
|
||||
var origin_name sql.NullString
|
||||
var origin_path sql.NullString
|
||||
var origin_ts sql.NullInt64
|
||||
var origin_key sql.NullString
|
||||
var data []byte // the dashboard JSON
|
||||
var version int64
|
||||
|
||||
err := rows.Scan(&orgId, &dashboard_id, &dash.Name,
|
||||
&slug, &folder_uid,
|
||||
&created, &createdByID, &createdByName,
|
||||
&updated, &updatedByID, &updatedByName,
|
||||
&plugin_id,
|
||||
&origin_name, &origin_path, &origin_key, &origin_ts,
|
||||
&version,
|
||||
&row.Title, &data,
|
||||
)
|
||||
|
||||
row.token = &continueToken{orgId: orgId, id: dashboard_id}
|
||||
if err == nil {
|
||||
dash.ResourceVersion = fmt.Sprintf("%d", created.UnixMilli())
|
||||
dash.Namespace = a.namespacer(orgId)
|
||||
dash.UID = utils.CalculateClusterWideUID(dash)
|
||||
dash.SetCreationTimestamp(v1.NewTime(created))
|
||||
meta := kinds.MetaAccessor(dash)
|
||||
meta.SetUpdatedTimestamp(&updated)
|
||||
meta.SetSlug(slug)
|
||||
if createdByID > 0 {
|
||||
meta.SetCreatedBy(fmt.Sprintf("user:%d/%s", createdByID, createdByName.String))
|
||||
}
|
||||
if updatedByID > 0 {
|
||||
meta.SetUpdatedBy(fmt.Sprintf("user:%d/%s", updatedByID, updatedByName.String))
|
||||
}
|
||||
if folder_uid.Valid {
|
||||
meta.SetFolder(folder_uid.String)
|
||||
row.FolderUID = folder_uid.String
|
||||
}
|
||||
|
||||
if origin_name.Valid {
|
||||
ts := time.Unix(origin_ts.Int64, 0)
|
||||
|
||||
originPath, err := filepath.Rel(
|
||||
a.provisioning.GetDashboardProvisionerResolvedPath(origin_name.String),
|
||||
origin_path.String,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
meta.SetOriginInfo(&kinds.ResourceOriginInfo{
|
||||
Name: origin_name.String,
|
||||
Path: originPath,
|
||||
Key: origin_key.String,
|
||||
Timestamp: &ts,
|
||||
})
|
||||
} else if plugin_id != "" {
|
||||
meta.SetOriginInfo(&kinds.ResourceOriginInfo{
|
||||
Name: "plugin",
|
||||
Path: plugin_id,
|
||||
})
|
||||
}
|
||||
|
||||
row.Bytes = len(data)
|
||||
if row.Bytes > 0 {
|
||||
err = dash.Spec.UnmarshalJSON(data)
|
||||
if err != nil {
|
||||
return row, err
|
||||
}
|
||||
dash.Spec.Set("id", dashboard_id) // add it so we can get it from the body later
|
||||
row.Title = dash.Spec.GetNestedString("title")
|
||||
row.Tags = dash.Spec.GetNestedStringSlice("tags")
|
||||
}
|
||||
}
|
||||
return row, err
|
||||
}
|
||||
|
||||
// DeleteDashboard implements DashboardAccess.
|
||||
func (a *dashboardSqlAccess) DeleteDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, bool, error) {
|
||||
dash, err := a.GetDashboard(ctx, orgId, uid)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
id := dash.Spec.GetNestedInt64("id")
|
||||
if id == 0 {
|
||||
return nil, false, fmt.Errorf("could not find id in saved body")
|
||||
}
|
||||
|
||||
err = a.dashStore.DeleteDashboard(ctx, &dashboards.DeleteDashboardCommand{
|
||||
OrgID: orgId,
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return dash, true, nil
|
||||
}
|
||||
|
||||
// SaveDashboard implements DashboardAccess.
|
||||
func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, dash *dashboardsV0.Dashboard) (*dashboardsV0.Dashboard, bool, error) {
|
||||
created := false
|
||||
user, err := appcontext.User(ctx)
|
||||
if err != nil {
|
||||
return nil, created, err
|
||||
}
|
||||
if dash.Name != "" {
|
||||
dash.Spec.Set("uid", dash.Name)
|
||||
|
||||
// Get the previous version to set the internal ID
|
||||
old, _ := a.dashStore.GetDashboard(ctx, &dashboards.GetDashboardQuery{
|
||||
OrgID: orgId,
|
||||
UID: dash.Name,
|
||||
})
|
||||
if old != nil {
|
||||
dash.Spec.Set("id", old.ID)
|
||||
} else {
|
||||
dash.Spec.Remove("id") // existing of "id" makes it an update
|
||||
created = true
|
||||
}
|
||||
} else {
|
||||
dash.Spec.Remove("id")
|
||||
dash.Spec.Remove("uid")
|
||||
}
|
||||
|
||||
meta := kinds.MetaAccessor(dash)
|
||||
out, err := a.dashStore.SaveDashboard(ctx, dashboards.SaveDashboardCommand{
|
||||
OrgID: orgId,
|
||||
Dashboard: simplejson.NewFromAny(dash.Spec.UnstructuredContent()),
|
||||
FolderUID: meta.GetFolder(),
|
||||
Overwrite: true, // already passed the revisionVersion checks!
|
||||
UserID: user.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if out != nil {
|
||||
created = (out.Created.Unix() == out.Updated.Unix()) // and now?
|
||||
}
|
||||
dash, err = a.GetDashboard(ctx, orgId, out.UID)
|
||||
return dash, created, err
|
||||
}
|
63
pkg/registry/apis/dashboard/access/token.go
Normal file
63
pkg/registry/apis/dashboard/access/token.go
Normal file
@ -0,0 +1,63 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type continueToken struct {
|
||||
orgId int64
|
||||
id int64 // the internal id (sort by!)
|
||||
folder string // from the query
|
||||
size int64
|
||||
}
|
||||
|
||||
func readContinueToken(q *DashboardQuery) (continueToken, error) {
|
||||
var err error
|
||||
token := continueToken{}
|
||||
if q.ContinueToken == "" {
|
||||
return token, nil
|
||||
}
|
||||
parts := strings.Split(q.ContinueToken, "/")
|
||||
if len(parts) < 3 {
|
||||
return token, fmt.Errorf("invalid continue token (too few parts)")
|
||||
}
|
||||
sub := strings.Split(parts[0], ":")
|
||||
if sub[0] != "org" {
|
||||
return token, fmt.Errorf("expected org in first slug")
|
||||
}
|
||||
token.orgId, err = strconv.ParseInt(sub[1], 10, 64)
|
||||
if err != nil {
|
||||
return token, fmt.Errorf("error parsing orgid")
|
||||
}
|
||||
|
||||
sub = strings.Split(parts[1], ":")
|
||||
if sub[0] != "start" {
|
||||
return token, fmt.Errorf("expected internal ID in second slug")
|
||||
}
|
||||
token.id, err = strconv.ParseInt(sub[1], 10, 64)
|
||||
if err != nil {
|
||||
return token, fmt.Errorf("error parsing updated")
|
||||
}
|
||||
|
||||
sub = strings.Split(parts[2], ":")
|
||||
if sub[0] != "folder" {
|
||||
return token, fmt.Errorf("expected folder UID in third slug")
|
||||
}
|
||||
token.folder = sub[1]
|
||||
|
||||
// Check if the folder filter is the same from the previous query
|
||||
if token.folder != q.FolderUID {
|
||||
return token, fmt.Errorf("invalid token, the folder must match previous query")
|
||||
}
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (r *continueToken) String() string {
|
||||
return fmt.Sprintf("org:%d/start:%d/folder:%s/%s",
|
||||
r.orgId, r.id, r.folder, util.ByteCountSI(r.size))
|
||||
}
|
31
pkg/registry/apis/dashboard/access/types.go
Normal file
31
pkg/registry/apis/dashboard/access/types.go
Normal file
@ -0,0 +1,31 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
)
|
||||
|
||||
// This does not check if you have permissions!
|
||||
|
||||
type DashboardQuery struct {
|
||||
OrgID int64
|
||||
UID string // to select a single dashboard
|
||||
FolderUID string
|
||||
Limit int
|
||||
MaxBytes int
|
||||
|
||||
// The token from previous query
|
||||
ContinueToken string
|
||||
}
|
||||
|
||||
type DashboardAccess interface {
|
||||
GetDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, error)
|
||||
GetDashboards(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardList, error)
|
||||
|
||||
GetDashboardSummary(ctx context.Context, orgId int64, uid string) (*dashboardsV0.DashboardSummary, error)
|
||||
GetDashboardSummaries(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardSummaryList, error)
|
||||
|
||||
SaveDashboard(ctx context.Context, orgId int64, dash *dashboardsV0.Dashboard) (*dashboardsV0.Dashboard, bool, error)
|
||||
DeleteDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, bool, error)
|
||||
}
|
95
pkg/registry/apis/dashboard/authorizer.go
Normal file
95
pkg/registry/apis/dashboard/authorizer.go
Normal file
@ -0,0 +1,95 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/appcontext"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
)
|
||||
|
||||
func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||
return authorizer.AuthorizerFunc(
|
||||
func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
if !attr.IsResourceRequest() {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
user, err := appcontext.User(ctx)
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
|
||||
if attr.GetName() == "" {
|
||||
// Discourage use of the "list" command for non super admin users
|
||||
if attr.GetVerb() == "list" && attr.GetResource() == v0alpha1.DashboardResourceInfo.GroupResource().Resource {
|
||||
if !user.IsGrafanaAdmin {
|
||||
return authorizer.DecisionDeny, "list summary objects (or connect as GrafanaAdmin)", err
|
||||
}
|
||||
}
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
ns := attr.GetNamespace()
|
||||
if ns == "" {
|
||||
return authorizer.DecisionDeny, "expected namespace", nil
|
||||
}
|
||||
|
||||
info, err := request.ParseNamespace(attr.GetNamespace())
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "error reading org from namespace", err
|
||||
}
|
||||
|
||||
// expensive path to lookup permissions for a single dashboard
|
||||
dto, err := b.dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{
|
||||
UID: attr.GetName(),
|
||||
OrgID: info.OrgID,
|
||||
})
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "error loading dashboard", err
|
||||
}
|
||||
|
||||
ok := false
|
||||
guardian, err := guardian.NewByDashboard(ctx, dto, info.OrgID, user)
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
|
||||
switch attr.GetVerb() {
|
||||
case "get":
|
||||
ok, err = guardian.CanView()
|
||||
if !ok || err != nil {
|
||||
return authorizer.DecisionDeny, "can not view dashboard", err
|
||||
}
|
||||
case "create":
|
||||
fallthrough
|
||||
case "post":
|
||||
ok, err = guardian.CanSave() // vs Edit?
|
||||
if !ok || err != nil {
|
||||
return authorizer.DecisionDeny, "can not save dashboard", err
|
||||
}
|
||||
case "update":
|
||||
fallthrough
|
||||
case "patch":
|
||||
fallthrough
|
||||
case "put":
|
||||
ok, err = guardian.CanEdit() // vs Save
|
||||
if !ok || err != nil {
|
||||
return authorizer.DecisionDeny, "can not edit dashboard", err
|
||||
}
|
||||
case "delete":
|
||||
ok, err = guardian.CanDelete()
|
||||
if !ok || err != nil {
|
||||
return authorizer.DecisionDeny, "can not delete dashboard", err
|
||||
}
|
||||
default:
|
||||
b.log.Info("unknown verb", "verb", attr.GetVerb())
|
||||
return authorizer.DecisionNoOpinion, "unsupported verb", nil // Unknown verb
|
||||
}
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
})
|
||||
}
|
155
pkg/registry/apis/dashboard/legacy_storage.go
Normal file
155
pkg/registry/apis/dashboard/legacy_storage.go
Normal file
@ -0,0 +1,155 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis"
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/access"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
|
||||
)
|
||||
|
||||
var (
|
||||
_ rest.Storage = (*dashboardStorage)(nil)
|
||||
_ rest.Scoper = (*dashboardStorage)(nil)
|
||||
_ rest.SingularNameProvider = (*dashboardStorage)(nil)
|
||||
_ rest.Getter = (*dashboardStorage)(nil)
|
||||
_ rest.Lister = (*dashboardStorage)(nil)
|
||||
_ rest.Creater = (*dashboardStorage)(nil)
|
||||
_ rest.Updater = (*dashboardStorage)(nil)
|
||||
_ rest.GracefulDeleter = (*dashboardStorage)(nil)
|
||||
)
|
||||
|
||||
type dashboardStorage struct {
|
||||
resource apis.ResourceInfo
|
||||
access access.DashboardAccess
|
||||
tableConverter rest.TableConvertor
|
||||
}
|
||||
|
||||
func (s *dashboardStorage) New() runtime.Object {
|
||||
return s.resource.NewFunc()
|
||||
}
|
||||
|
||||
func (s *dashboardStorage) Destroy() {}
|
||||
|
||||
func (s *dashboardStorage) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *dashboardStorage) GetSingularName() string {
|
||||
return s.resource.GetSingularName()
|
||||
}
|
||||
|
||||
func (s *dashboardStorage) NewList() runtime.Object {
|
||||
return s.resource.NewListFunc()
|
||||
}
|
||||
|
||||
func (s *dashboardStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
func (s *dashboardStorage) Create(ctx context.Context,
|
||||
obj runtime.Object,
|
||||
createValidation rest.ValidateObjectFunc,
|
||||
options *metav1.CreateOptions,
|
||||
) (runtime.Object, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p, ok := obj.(*v0alpha1.Dashboard)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected dashboard?")
|
||||
}
|
||||
|
||||
// HACK to simplify unique name testing from kubectl
|
||||
t := p.Spec.GetNestedString("title")
|
||||
if strings.Contains(t, "${NOW}") {
|
||||
t = strings.ReplaceAll(t, "${NOW}", fmt.Sprintf("%d", time.Now().Unix()))
|
||||
p.Spec.Set("title", t)
|
||||
}
|
||||
|
||||
dash, _, err := s.access.SaveDashboard(ctx, info.OrgID, p)
|
||||
return dash, err
|
||||
}
|
||||
|
||||
func (s *dashboardStorage) Update(ctx context.Context,
|
||||
name string,
|
||||
objInfo rest.UpdatedObjectInfo,
|
||||
createValidation rest.ValidateObjectFunc,
|
||||
updateValidation rest.ValidateObjectUpdateFunc,
|
||||
forceAllowCreate bool,
|
||||
options *metav1.UpdateOptions,
|
||||
) (runtime.Object, bool, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
created := false
|
||||
old, err := s.Get(ctx, name, nil)
|
||||
if err != nil {
|
||||
return old, created, err
|
||||
}
|
||||
|
||||
obj, err := objInfo.UpdatedObject(ctx, old)
|
||||
if err != nil {
|
||||
return old, created, err
|
||||
}
|
||||
p, ok := obj.(*v0alpha1.Dashboard)
|
||||
if !ok {
|
||||
return nil, created, fmt.Errorf("expected dashboard after update")
|
||||
}
|
||||
|
||||
_, created, err = s.access.SaveDashboard(ctx, info.OrgID, p)
|
||||
if err == nil {
|
||||
r, err := s.Get(ctx, name, nil)
|
||||
return r, created, err
|
||||
}
|
||||
return nil, created, err
|
||||
}
|
||||
|
||||
// GracefulDeleter
|
||||
func (s *dashboardStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return s.access.DeleteDashboard(ctx, info.OrgID, name)
|
||||
}
|
||||
|
||||
func (s *dashboardStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
orgId, err := request.OrgIDForList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// fmt.Printf("LIST: %s\n", options.Continue)
|
||||
|
||||
query := &access.DashboardQuery{
|
||||
OrgID: orgId,
|
||||
Limit: int(options.Limit),
|
||||
MaxBytes: 2 * 1024 * 1024, // 2MB,
|
||||
ContinueToken: options.Continue,
|
||||
}
|
||||
return s.access.GetDashboards(ctx, query)
|
||||
}
|
||||
|
||||
func (s *dashboardStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.access.GetDashboard(ctx, info.OrgID, name)
|
||||
}
|
200
pkg/registry/apis/dashboard/register.go
Normal file
200
pkg/registry/apis/dashboard/register.go
Normal file
@ -0,0 +1,200 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
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/registry/generic"
|
||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
common "k8s.io/kube-openapi/pkg/common"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/access"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/services/grafana-apiserver/registry/generic"
|
||||
grafanarest "github.com/grafana/grafana/pkg/services/grafana-apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/utils"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var _ grafanaapiserver.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
|
||||
|
||||
// This is used just so wire has something unique to return
|
||||
type DashboardsAPIBuilder struct {
|
||||
dashboardService dashboards.DashboardService
|
||||
|
||||
dashboardVersionService dashver.Service
|
||||
accessControl accesscontrol.AccessControl
|
||||
namespacer request.NamespaceMapper
|
||||
access access.DashboardAccess
|
||||
dashStore dashboards.Store
|
||||
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
apiregistration grafanaapiserver.APIRegistrar,
|
||||
dashboardService dashboards.DashboardService,
|
||||
dashboardVersionService dashver.Service,
|
||||
accessControl accesscontrol.AccessControl,
|
||||
provisioning provisioning.ProvisioningService,
|
||||
dashStore dashboards.Store,
|
||||
sql db.DB,
|
||||
) *DashboardsAPIBuilder {
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
return nil // skip registration unless opting into experimental apis
|
||||
}
|
||||
|
||||
namespacer := request.GetNamespaceMapper(cfg)
|
||||
builder := &DashboardsAPIBuilder{
|
||||
dashboardService: dashboardService,
|
||||
dashboardVersionService: dashboardVersionService,
|
||||
dashStore: dashStore,
|
||||
accessControl: accessControl,
|
||||
namespacer: namespacer,
|
||||
access: access.NewDashboardAccess(sql, namespacer, dashStore, provisioning),
|
||||
log: log.New("grafana-apiserver.dashboards"),
|
||||
}
|
||||
apiregistration.RegisterAPI(builder)
|
||||
return builder
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
||||
return v0alpha1.DashboardResourceInfo.GroupVersion()
|
||||
}
|
||||
|
||||
func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
|
||||
scheme.AddKnownTypes(gv,
|
||||
&v0alpha1.Dashboard{},
|
||||
&v0alpha1.DashboardList{},
|
||||
&v0alpha1.DashboardAccessInfo{},
|
||||
&v0alpha1.DashboardVersionsInfo{},
|
||||
&v0alpha1.DashboardSummary{},
|
||||
&v0alpha1.DashboardSummaryList{},
|
||||
&v0alpha1.VersionsQueryOptions{},
|
||||
)
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
resourceInfo := v0alpha1.DashboardResourceInfo
|
||||
addKnownTypes(scheme, resourceInfo.GroupVersion())
|
||||
|
||||
// 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: resourceInfo.GroupVersion().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, resourceInfo.GroupVersion())
|
||||
return scheme.SetVersionPriority(resourceInfo.GroupVersion())
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) GetAPIGroupInfo(
|
||||
scheme *runtime.Scheme,
|
||||
codecs serializer.CodecFactory, // pointer?
|
||||
optsGetter generic.RESTOptionsGetter,
|
||||
) (*genericapiserver.APIGroupInfo, error) {
|
||||
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(v0alpha1.GROUP, scheme, metav1.ParameterCodec, codecs)
|
||||
|
||||
resourceInfo := v0alpha1.DashboardResourceInfo
|
||||
strategy := grafanaregistry.NewStrategy(scheme)
|
||||
store := &genericregistry.Store{
|
||||
NewFunc: resourceInfo.NewFunc,
|
||||
NewListFunc: resourceInfo.NewListFunc,
|
||||
PredicateFunc: grafanaregistry.Matcher,
|
||||
DefaultQualifiedResource: resourceInfo.GroupResource(),
|
||||
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
|
||||
CreateStrategy: strategy,
|
||||
UpdateStrategy: strategy,
|
||||
DeleteStrategy: strategy,
|
||||
}
|
||||
store.TableConvertor = utils.NewTableConverter(
|
||||
store.DefaultQualifiedResource,
|
||||
[]metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Title", Type: "string", Format: "string", Description: "The dashboard name"},
|
||||
{Name: "Created At", Type: "date"},
|
||||
},
|
||||
func(obj any) ([]interface{}, error) {
|
||||
dash, ok := obj.(*v0alpha1.Dashboard)
|
||||
if ok {
|
||||
return []interface{}{
|
||||
dash.Name,
|
||||
dash.Spec.GetNestedString("title"),
|
||||
dash.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
summary, ok := obj.(*v0alpha1.DashboardSummary)
|
||||
if ok {
|
||||
return []interface{}{
|
||||
dash.Name,
|
||||
summary.Spec.Title,
|
||||
dash.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expected dashboard or summary")
|
||||
})
|
||||
|
||||
legacyStore := &dashboardStorage{
|
||||
resource: resourceInfo,
|
||||
access: b.access,
|
||||
tableConverter: store.TableConvertor,
|
||||
}
|
||||
|
||||
storage := map[string]rest.Storage{}
|
||||
storage[resourceInfo.StoragePath()] = legacyStore
|
||||
storage[resourceInfo.StoragePath("access")] = &AccessREST{
|
||||
builder: b,
|
||||
}
|
||||
storage[resourceInfo.StoragePath("versions")] = &VersionsREST{
|
||||
builder: b,
|
||||
}
|
||||
|
||||
// Dual writes if a RESTOptionsGetter is provided
|
||||
if optsGetter != nil {
|
||||
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs}
|
||||
if err := store.CompleteWithOptions(options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storage[resourceInfo.StoragePath()] = grafanarest.NewDualWriter(legacyStore, store)
|
||||
}
|
||||
|
||||
// Summary
|
||||
resourceInfo = v0alpha1.DashboardSummaryResourceInfo
|
||||
storage[resourceInfo.StoragePath()] = &summaryStorage{
|
||||
resource: resourceInfo,
|
||||
access: b.access,
|
||||
tableConverter: store.TableConvertor,
|
||||
}
|
||||
|
||||
apiGroupInfo.VersionedResourcesStorageMap[v0alpha1.VERSION] = storage
|
||||
return &apiGroupInfo, nil
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
|
||||
return v0alpha1.GetOpenAPIDefinitions
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes {
|
||||
return nil // no custom API routes
|
||||
}
|
105
pkg/registry/apis/dashboard/sub_access.go
Normal file
105
pkg/registry/apis/dashboard/sub_access.go
Normal file
@ -0,0 +1,105 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/appcontext"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
dashboardssvc "github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
)
|
||||
|
||||
type AccessREST struct {
|
||||
builder *DashboardsAPIBuilder
|
||||
}
|
||||
|
||||
var _ = rest.Connecter(&AccessREST{})
|
||||
|
||||
func (r *AccessREST) New() runtime.Object {
|
||||
return &v0alpha1.DashboardAccessInfo{}
|
||||
}
|
||||
|
||||
func (r *AccessREST) Destroy() {
|
||||
}
|
||||
|
||||
func (r *AccessREST) ConnectMethods() []string {
|
||||
return []string{"GET"}
|
||||
}
|
||||
|
||||
func (r *AccessREST) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return &v0alpha1.VersionsQueryOptions{}, false, ""
|
||||
}
|
||||
|
||||
func (r *AccessREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := appcontext.User(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dto, err := r.builder.dashboardService.GetDashboard(ctx, &dashboardssvc.GetDashboardQuery{
|
||||
UID: name,
|
||||
OrgID: info.OrgID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
guardian, err := guardian.NewByDashboard(ctx, dto, info.OrgID, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
canView, err := guardian.CanView()
|
||||
if err != nil || !canView {
|
||||
return nil, fmt.Errorf("not allowed to view")
|
||||
}
|
||||
|
||||
access := &v0alpha1.DashboardAccessInfo{}
|
||||
access.CanEdit, _ = guardian.CanEdit()
|
||||
access.CanSave, _ = guardian.CanSave()
|
||||
access.CanAdmin, _ = guardian.CanAdmin()
|
||||
access.CanDelete, _ = guardian.CanDelete()
|
||||
access.CanStar = user.IsRealUser() && !user.IsAnonymous
|
||||
|
||||
access.AnnotationsPermissions = &v0alpha1.AnnotationPermission{}
|
||||
r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Dashboard, accesscontrol.ScopeAnnotationsTypeDashboard)
|
||||
r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Organization, accesscontrol.ScopeAnnotationsTypeOrganization)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
responder.Object(http.StatusOK, access)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (r *AccessREST) getAnnotationPermissionsByScope(ctx context.Context, user identity.Requester, actions *v0alpha1.AnnotationActions, scope string) {
|
||||
var err error
|
||||
|
||||
evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, scope)
|
||||
actions.CanAdd, err = r.builder.accessControl.Evaluate(ctx, user, evaluate)
|
||||
if err != nil {
|
||||
r.builder.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsCreate, "scope", scope)
|
||||
}
|
||||
|
||||
evaluate = accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, scope)
|
||||
actions.CanDelete, err = r.builder.accessControl.Evaluate(ctx, user, evaluate)
|
||||
if err != nil {
|
||||
r.builder.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsDelete, "scope", scope)
|
||||
}
|
||||
|
||||
evaluate = accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsWrite, scope)
|
||||
actions.CanEdit, err = r.builder.accessControl.Evaluate(ctx, user, evaluate)
|
||||
if err != nil {
|
||||
r.builder.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsWrite, "scope", scope)
|
||||
}
|
||||
}
|
107
pkg/registry/apis/dashboard/sub_versions.go
Normal file
107
pkg/registry/apis/dashboard/sub_versions.go
Normal file
@ -0,0 +1,107 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
|
||||
)
|
||||
|
||||
type VersionsREST struct {
|
||||
builder *DashboardsAPIBuilder
|
||||
}
|
||||
|
||||
var _ = rest.Connecter(&VersionsREST{})
|
||||
|
||||
func (r *VersionsREST) New() runtime.Object {
|
||||
return &v0alpha1.DashboardVersionsInfo{}
|
||||
}
|
||||
|
||||
func (r *VersionsREST) Destroy() {
|
||||
}
|
||||
|
||||
func (r *VersionsREST) ConnectMethods() []string {
|
||||
return []string{"GET"}
|
||||
}
|
||||
|
||||
func (r *VersionsREST) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, true, ""
|
||||
}
|
||||
|
||||
func (r *VersionsREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
idx := strings.LastIndex(path, "/versions/")
|
||||
if idx > 0 {
|
||||
key := path[strings.LastIndex(path, "/")+1:]
|
||||
version, err := strconv.Atoi(key)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
dto, err := r.builder.dashboardVersionService.Get(ctx, &dashver.GetDashboardVersionQuery{
|
||||
DashboardUID: uid,
|
||||
OrgID: info.OrgID,
|
||||
Version: version,
|
||||
})
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
data, _ := dto.Data.Map()
|
||||
|
||||
// Convert the version to a regular dashboard
|
||||
dash := &v0alpha1.Dashboard{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: uid,
|
||||
CreationTimestamp: metav1.NewTime(dto.Created),
|
||||
},
|
||||
Spec: v0alpha1.Unstructured{Object: data},
|
||||
}
|
||||
responder.Object(100, dash)
|
||||
return
|
||||
}
|
||||
|
||||
// Or list versions
|
||||
rsp, err := r.builder.dashboardVersionService.List(ctx, &dashver.ListDashboardVersionsQuery{
|
||||
DashboardUID: uid,
|
||||
OrgID: info.OrgID,
|
||||
})
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
versions := &v0alpha1.DashboardVersionsInfo{}
|
||||
for _, v := range rsp {
|
||||
info := v0alpha1.DashboardVersionInfo{
|
||||
Version: v.Version,
|
||||
Created: v.Created.UnixMilli(),
|
||||
Message: v.Message,
|
||||
}
|
||||
if v.ParentVersion != v.Version {
|
||||
info.ParentVersion = v.ParentVersion
|
||||
}
|
||||
if v.CreatedBy > 0 {
|
||||
info.CreatedBy = fmt.Sprintf("%d", v.CreatedBy)
|
||||
}
|
||||
versions.Items = append(versions.Items, info)
|
||||
}
|
||||
responder.Object(http.StatusOK, versions)
|
||||
}), nil
|
||||
}
|
74
pkg/registry/apis/dashboard/summary_storage.go
Normal file
74
pkg/registry/apis/dashboard/summary_storage.go
Normal file
@ -0,0 +1,74 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/access"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
|
||||
)
|
||||
|
||||
var (
|
||||
_ rest.Storage = (*summaryStorage)(nil)
|
||||
_ rest.Scoper = (*summaryStorage)(nil)
|
||||
_ rest.SingularNameProvider = (*summaryStorage)(nil)
|
||||
_ rest.Getter = (*summaryStorage)(nil)
|
||||
_ rest.Lister = (*summaryStorage)(nil)
|
||||
)
|
||||
|
||||
type summaryStorage struct {
|
||||
resource apis.ResourceInfo
|
||||
access access.DashboardAccess
|
||||
tableConverter rest.TableConvertor
|
||||
}
|
||||
|
||||
func (s *summaryStorage) New() runtime.Object {
|
||||
return s.resource.NewFunc()
|
||||
}
|
||||
|
||||
func (s *summaryStorage) Destroy() {}
|
||||
|
||||
func (s *summaryStorage) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *summaryStorage) GetSingularName() string {
|
||||
return s.resource.GetSingularName()
|
||||
}
|
||||
|
||||
func (s *summaryStorage) NewList() runtime.Object {
|
||||
return s.resource.NewListFunc()
|
||||
}
|
||||
|
||||
func (s *summaryStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
func (s *summaryStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
orgId, err := request.OrgIDForList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := &access.DashboardQuery{
|
||||
OrgID: orgId,
|
||||
Limit: int(options.Limit),
|
||||
MaxBytes: 2 * 1024 * 1024, // 2MB,
|
||||
ContinueToken: options.Continue,
|
||||
}
|
||||
return s.access.GetDashboardSummaries(ctx, query)
|
||||
}
|
||||
|
||||
func (s *summaryStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.access.GetDashboardSummary(ctx, info.OrgID, name)
|
||||
}
|
@ -3,6 +3,7 @@ package apiregistry
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/example"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/folders"
|
||||
@ -14,6 +15,7 @@ var WireSet = wire.NewSet(
|
||||
|
||||
// Each must be added here *and* in the ServiceSink above
|
||||
playlist.RegisterAPIService,
|
||||
dashboard.RegisterAPIService,
|
||||
example.RegisterAPIService,
|
||||
datasource.RegisterAPIService,
|
||||
folders.RegisterAPIService,
|
||||
|
@ -4,12 +4,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/slugify"
|
||||
"github.com/grafana/grafana/pkg/kinds"
|
||||
"github.com/grafana/grafana/pkg/kinds/dashboard"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||
@ -67,55 +63,6 @@ func (d *Dashboard) SetVersion(version int) {
|
||||
d.Data.Set("version", version)
|
||||
}
|
||||
|
||||
func (d *Dashboard) ToResource() kinds.GrafanaResource[simplejson.Json, any] {
|
||||
parent := dashboard.NewK8sResource(d.UID, nil)
|
||||
res := kinds.GrafanaResource[simplejson.Json, any]{
|
||||
Kind: parent.Kind,
|
||||
APIVersion: parent.APIVersion,
|
||||
Metadata: kinds.GrafanaResourceMetadata{
|
||||
Name: d.UID,
|
||||
Annotations: make(map[string]string),
|
||||
Labels: make(map[string]string),
|
||||
CreationTimestamp: v1.NewTime(d.Created),
|
||||
ResourceVersion: fmt.Sprintf("%d", d.Version),
|
||||
},
|
||||
}
|
||||
if d.Data != nil {
|
||||
copy := &simplejson.Json{}
|
||||
db, _ := d.Data.ToDB()
|
||||
_ = copy.FromDB(db)
|
||||
|
||||
copy.Del("id")
|
||||
copy.Del("version") // ???
|
||||
copy.Del("uid") // duplicated to name
|
||||
res.Spec = copy
|
||||
}
|
||||
|
||||
d.UpdateSlug()
|
||||
res.Metadata.SetUpdatedTimestamp(&d.Updated)
|
||||
res.Metadata.SetSlug(d.Slug)
|
||||
if d.CreatedBy > 0 {
|
||||
res.Metadata.SetCreatedBy(fmt.Sprintf("user:%d", d.CreatedBy))
|
||||
}
|
||||
if d.UpdatedBy > 0 {
|
||||
res.Metadata.SetUpdatedBy(fmt.Sprintf("user:%d", d.UpdatedBy))
|
||||
}
|
||||
if d.PluginID != "" {
|
||||
res.Metadata.SetOriginInfo(&kinds.ResourceOriginInfo{
|
||||
Name: "plugin",
|
||||
Key: d.PluginID,
|
||||
})
|
||||
}
|
||||
// nolint:staticcheck
|
||||
if d.FolderID > 0 {
|
||||
res.Metadata.SetFolder(fmt.Sprintf("folder:%d", d.FolderID))
|
||||
}
|
||||
if d.IsFolder {
|
||||
res.Kind = "Folder"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// NewDashboard creates a new dashboard
|
||||
func NewDashboard(title string) *Dashboard {
|
||||
dash := &Dashboard{}
|
||||
|
@ -1,10 +1,7 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -91,54 +88,3 @@ func TestSlugifyTitle(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceConversion(t *testing.T) {
|
||||
body := simplejson.New()
|
||||
body.Set("title", "test dash")
|
||||
body.Set("tags", []string{"hello", "world"})
|
||||
|
||||
dash := NewDashboardFromJson(body)
|
||||
dash.SetUID("TheUID")
|
||||
dash.SetVersion(10)
|
||||
dash.Created = time.UnixMilli(946713600000).UTC() // 2000-01-01
|
||||
dash.Updated = time.UnixMilli(1262332800000).UTC() // 2010-01-01
|
||||
dash.CreatedBy = 10
|
||||
dash.UpdatedBy = 11
|
||||
dash.PluginID = "plugin-xyz"
|
||||
// nolint:staticcheck
|
||||
dash.FolderID = 1234
|
||||
dash.SetID(12345) // should be removed in resource version
|
||||
|
||||
dst := dash.ToResource()
|
||||
require.Equal(t, int64(12345), dash.ID)
|
||||
require.Equal(t, int64(12345), dash.Data.Get("id").MustInt64(0))
|
||||
|
||||
out, err := json.MarshalIndent(dst, "", " ")
|
||||
require.NoError(t, err)
|
||||
fmt.Printf("%s", string(out))
|
||||
require.JSONEq(t, `{
|
||||
"apiVersion": "v0-0-alpha",
|
||||
"kind": "Dashboard",
|
||||
"metadata": {
|
||||
"name": "TheUID",
|
||||
"resourceVersion": "10",
|
||||
"creationTimestamp": "2000-01-01T08:00:00Z",
|
||||
"annotations": {
|
||||
"grafana.app/createdBy": "user:10",
|
||||
"grafana.app/folder": "folder:1234",
|
||||
"grafana.app/originKey": "plugin-xyz",
|
||||
"grafana.app/originName": "plugin",
|
||||
"grafana.app/slug": "test-dash",
|
||||
"grafana.app/updatedBy": "user:11",
|
||||
"grafana.app/updatedTimestamp": "2010-01-01T08:00:00Z"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"tags": [
|
||||
"hello",
|
||||
"world"
|
||||
],
|
||||
"title": "test dash"
|
||||
}
|
||||
}`, string(out))
|
||||
}
|
||||
|
114
pkg/tests/apis/dashboard/dashboards_test.go
Normal file
114
pkg/tests/apis/dashboard/dashboards_test.go
Normal file
@ -0,0 +1,114 @@
|
||||
package dashboards
|
||||
|
||||
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 TestRequiresDevMode(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: true, // should fail
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagGrafanaAPIServer,
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service
|
||||
},
|
||||
})
|
||||
|
||||
_, err := helper.NewDiscoveryClient().ServerResourcesForGroupVersion("dashboard.grafana.app/v0alpha1")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDashboardsApp(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.FlagGrafanaAPIServer,
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service
|
||||
},
|
||||
})
|
||||
_, err := helper.NewDiscoveryClient().ServerResourcesForGroupVersion("dashboard.grafana.app/v0alpha1")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("Check discovery client", func(t *testing.T) {
|
||||
disco := helper.GetGroupVersionInfoJSON("dashboard.grafana.app")
|
||||
// fmt.Printf("%s", string(disco))
|
||||
|
||||
require.JSONEq(t, `[
|
||||
{
|
||||
"freshness": "Current",
|
||||
"resources": [
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "Dashboard",
|
||||
"version": ""
|
||||
},
|
||||
"scope": "Namespaced",
|
||||
"singularResource": "dashboard",
|
||||
"subresources": [
|
||||
{
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "DashboardAccessInfo",
|
||||
"version": ""
|
||||
},
|
||||
"subresource": "access",
|
||||
"verbs": [
|
||||
"get"
|
||||
]
|
||||
},
|
||||
{
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "DashboardVersionsInfo",
|
||||
"version": ""
|
||||
},
|
||||
"subresource": "versions",
|
||||
"verbs": [
|
||||
"get"
|
||||
]
|
||||
}
|
||||
],
|
||||
"verbs": [
|
||||
"create",
|
||||
"delete",
|
||||
"get",
|
||||
"list",
|
||||
"patch",
|
||||
"update"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "summary",
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "DashboardSummary",
|
||||
"version": ""
|
||||
},
|
||||
"scope": "Namespaced",
|
||||
"singularResource": "summary",
|
||||
"verbs": [
|
||||
"get",
|
||||
"list"
|
||||
]
|
||||
}
|
||||
],
|
||||
"version": "v0alpha1"
|
||||
}
|
||||
]`, disco)
|
||||
})
|
||||
}
|
6
pkg/tests/apis/dashboard/testdata/dashboard-generate.yaml
vendored
Normal file
6
pkg/tests/apis/dashboard/testdata/dashboard-generate.yaml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: dashboard.grafana.app/v0alpha1
|
||||
kind: Dashboard
|
||||
metadata:
|
||||
generateName: x # anything is ok here... except yes or true -- they become boolean!
|
||||
spec:
|
||||
title: Dashboard with auto generated UID ${NOW}
|
6
pkg/tests/apis/dashboard/testdata/dashboard-test-apply.yaml
vendored
Normal file
6
pkg/tests/apis/dashboard/testdata/dashboard-test-apply.yaml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: dashboard.grafana.app/v0alpha1
|
||||
kind: Dashboard
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
title: Test dashboard (apply from k8s; PATCH) X
|
6
pkg/tests/apis/dashboard/testdata/dashboard-test-create.yaml
vendored
Normal file
6
pkg/tests/apis/dashboard/testdata/dashboard-test-create.yaml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: dashboard.grafana.app/v0alpha1
|
||||
kind: Dashboard
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
title: Test dashboard (created from k8s; POST)
|
6
pkg/tests/apis/dashboard/testdata/dashboard-test-replace.yaml
vendored
Normal file
6
pkg/tests/apis/dashboard/testdata/dashboard-test-replace.yaml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: dashboard.grafana.app/v0alpha1
|
||||
kind: Dashboard
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
title: Test dashboard (replaced from k8s; PUT)
|
Loading…
Reference in New Issue
Block a user