LibraryPanels: Expose library panels in dashboard apiserver (#92213)

pull/87644/head
Ryan McKinley 9 months ago committed by GitHub
parent 7ad3d9bf76
commit 419edef4dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 30
      pkg/apis/dashboard/v0alpha1/register.go
  2. 60
      pkg/apis/dashboard/v0alpha1/types.go
  3. 118
      pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go
  4. 215
      pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go
  5. 1
      pkg/apis/dashboard/v0alpha1/zz_generated.openapi_violation_exceptions.list
  6. 23
      pkg/registry/apis/dashboard/legacy/queries.go
  7. 29
      pkg/registry/apis/dashboard/legacy/queries_test.go
  8. 18
      pkg/registry/apis/dashboard/legacy/query_panels.sql
  9. 142
      pkg/registry/apis/dashboard/legacy/sql_dashboards.go
  10. 10
      pkg/registry/apis/dashboard/legacy/testdata/mysql--query_panels-get_uid.sql
  11. 10
      pkg/registry/apis/dashboard/legacy/testdata/mysql--query_panels-list.sql
  12. 10
      pkg/registry/apis/dashboard/legacy/testdata/mysql--query_panels-list_page_two.sql
  13. 10
      pkg/registry/apis/dashboard/legacy/testdata/postgres--query_panels-get_uid.sql
  14. 10
      pkg/registry/apis/dashboard/legacy/testdata/postgres--query_panels-list.sql
  15. 10
      pkg/registry/apis/dashboard/legacy/testdata/postgres--query_panels-list_page_two.sql
  16. 10
      pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_panels-get_uid.sql
  17. 10
      pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_panels-list.sql
  18. 10
      pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_panels-list_page_two.sql
  19. 13
      pkg/registry/apis/dashboard/legacy/types.go
  20. 94
      pkg/registry/apis/dashboard/libary_panel.go
  21. 10
      pkg/registry/apis/dashboard/register.go
  22. 4
      pkg/registry/apis/dashboard/storage.go
  23. 11
      pkg/services/apiserver/builder/openapi.go
  24. 14
      pkg/tests/apis/dashboard/dashboards_test.go

@ -38,7 +38,35 @@ var DashboardResourceInfo = common.NewResourceInfo(GROUP, VERSION,
}, nil
}
}
return nil, fmt.Errorf("expected dashboard or summary")
return nil, fmt.Errorf("expected dashboard")
},
},
)
var LibraryPanelResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"librarypanels", "librarypanel", "LibraryPanel",
func() runtime.Object { return &LibraryPanel{} },
func() runtime.Object { return &LibraryPanelList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Description: "The dashboard name"},
{Name: "Type", Type: "string", Description: "the panel type"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
dash, ok := obj.(*LibraryPanel)
if ok {
if dash != nil {
return []interface{}{
dash.Name,
dash.Spec.Title,
dash.Spec.Type,
dash.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
}
}
return nil, fmt.Errorf("expected library panel")
},
},
)

@ -3,6 +3,7 @@ package v0alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
@ -66,6 +67,65 @@ type VersionsQueryOptions struct {
Version int64 `json:"version,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type LibraryPanel 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"`
// Panel properties
Spec LibraryPanelSpec `json:"spec"`
// Status will show errors
Status *LibraryPanelStatus `json:"status,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type LibraryPanelList struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ListMeta `json:"metadata,omitempty"`
Items []LibraryPanel `json:"items,omitempty"`
}
type LibraryPanelSpec struct {
// The panel type
Type string `json:"type"`
// The panel type
PluginVersion string `json:"pluginVersion,omitempty"`
// The panel title
Title string `json:"title,omitempty"`
// Library panel description
Description string `json:"description,omitempty"`
// The options schema depends on the panel type
Options common.Unstructured `json:"options"`
// The fieldConfig schema depends on the panel type
FieldConfig common.Unstructured `json:"fieldConfig"`
// The default datasource type
Datasource *data.DataSourceRef `json:"datasource,omitempty"`
// The datasource queries
// +listType=set
Targets []data.DataQuery `json:"targets,omitempty"`
}
type LibraryPanelStatus struct {
// Translation warnings (mostly things that were in SQL columns but not found in the saved body)
Warnings []string `json:"warnings,omitempty"`
// The properties previously stored in SQL that are not included in this model
Missing common.Unstructured `json:"missing,omitempty"`
}
// This is like the legacy DTO where access and metadata are all returned in a single call
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DashboardWithAccessInfo struct {

@ -8,6 +8,7 @@
package v0alpha1
import (
datav0alpha1 "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@ -199,6 +200,123 @@ func (in *DashboardWithAccessInfo) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LibraryPanel) DeepCopyInto(out *LibraryPanel) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
if in.Status != nil {
in, out := &in.Status, &out.Status
*out = new(LibraryPanelStatus)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibraryPanel.
func (in *LibraryPanel) DeepCopy() *LibraryPanel {
if in == nil {
return nil
}
out := new(LibraryPanel)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *LibraryPanel) 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 *LibraryPanelList) DeepCopyInto(out *LibraryPanelList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]LibraryPanel, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibraryPanelList.
func (in *LibraryPanelList) DeepCopy() *LibraryPanelList {
if in == nil {
return nil
}
out := new(LibraryPanelList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *LibraryPanelList) 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 *LibraryPanelSpec) DeepCopyInto(out *LibraryPanelSpec) {
*out = *in
in.Options.DeepCopyInto(&out.Options)
in.FieldConfig.DeepCopyInto(&out.FieldConfig)
if in.Datasource != nil {
in, out := &in.Datasource, &out.Datasource
*out = new(datav0alpha1.DataSourceRef)
**out = **in
}
if in.Targets != nil {
in, out := &in.Targets, &out.Targets
*out = make([]datav0alpha1.DataQuery, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibraryPanelSpec.
func (in *LibraryPanelSpec) DeepCopy() *LibraryPanelSpec {
if in == nil {
return nil
}
out := new(LibraryPanelSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LibraryPanelStatus) DeepCopyInto(out *LibraryPanelStatus) {
*out = *in
if in.Warnings != nil {
in, out := &in.Warnings, &out.Warnings
*out = make([]string, len(*in))
copy(*out, *in)
}
in.Missing.DeepCopyInto(&out.Missing)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibraryPanelStatus.
func (in *LibraryPanelStatus) DeepCopy() *LibraryPanelStatus {
if in == nil {
return nil
}
out := new(LibraryPanelStatus)
in.DeepCopyInto(out)
return out
}
// 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

@ -22,6 +22,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardVersionInfo": schema_pkg_apis_dashboard_v0alpha1_DashboardVersionInfo(ref),
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardVersionList": schema_pkg_apis_dashboard_v0alpha1_DashboardVersionList(ref),
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v0alpha1_DashboardWithAccessInfo(ref),
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.LibraryPanel": schema_pkg_apis_dashboard_v0alpha1_LibraryPanel(ref),
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.LibraryPanelList": schema_pkg_apis_dashboard_v0alpha1_LibraryPanelList(ref),
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.LibraryPanelSpec": schema_pkg_apis_dashboard_v0alpha1_LibraryPanelSpec(ref),
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.LibraryPanelStatus": schema_pkg_apis_dashboard_v0alpha1_LibraryPanelStatus(ref),
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.VersionsQueryOptions": schema_pkg_apis_dashboard_v0alpha1_VersionsQueryOptions(ref),
}
}
@ -392,6 +396,217 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardWithAccessInfo(ref common.Refer
}
}
func schema_pkg_apis_dashboard_v0alpha1_LibraryPanel(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{
Description: "Standard object's metadata More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata",
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Description: "Panel properties",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.LibraryPanelSpec"),
},
},
"status": {
SchemaProps: spec.SchemaProps{
Description: "Status will show errors",
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.LibraryPanelStatus"),
},
},
},
Required: []string{"spec"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.LibraryPanelSpec", "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.LibraryPanelStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_dashboard_v0alpha1_LibraryPanelList(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/dashboard/v0alpha1.LibraryPanel"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.LibraryPanel", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_dashboard_v0alpha1_LibraryPanelSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"type": {
SchemaProps: spec.SchemaProps{
Description: "The panel type",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"pluginVersion": {
SchemaProps: spec.SchemaProps{
Description: "The panel type",
Type: []string{"string"},
Format: "",
},
},
"title": {
SchemaProps: spec.SchemaProps{
Description: "The panel title",
Type: []string{"string"},
Format: "",
},
},
"description": {
SchemaProps: spec.SchemaProps{
Description: "Library panel description",
Type: []string{"string"},
Format: "",
},
},
"options": {
SchemaProps: spec.SchemaProps{
Description: "The options schema depends on the panel type",
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
},
},
"fieldConfig": {
SchemaProps: spec.SchemaProps{
Description: "The fieldConfig schema depends on the panel type",
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
},
},
"datasource": {
SchemaProps: spec.SchemaProps{
Description: "The default datasource type",
Ref: ref("github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataSourceRef"),
},
},
"targets": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "set",
},
},
SchemaProps: spec.SchemaProps{
Description: "The datasource queries",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery"),
},
},
},
},
},
},
Required: []string{"type", "options", "fieldConfig"},
},
},
Dependencies: []string{
"github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery", "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataSourceRef", "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"},
}
}
func schema_pkg_apis_dashboard_v0alpha1_LibraryPanelStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"warnings": {
SchemaProps: spec.SchemaProps{
Description: "Translation warnings (mostly things that were in SQL columns but not found in the saved body)",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
"missing": {
SchemaProps: spec.SchemaProps{
Description: "The properties previously stored in SQL that are not included in this model",
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"},
}
}
func schema_pkg_apis_dashboard_v0alpha1_VersionsQueryOptions(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{

@ -0,0 +1 @@
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1,LibraryPanelStatus,Warnings

@ -27,6 +27,7 @@ func mustTemplate(filename string) *template.Template {
// Templates.
var (
sqlQueryDashboards = mustTemplate("query_dashboards.sql")
sqlQueryPanels = mustTemplate("query_panels.sql")
)
type sqlQuery struct {
@ -54,3 +55,25 @@ func newQueryReq(sql *legacysql.LegacyDatabaseHelper, query *DashboardQuery) sql
UserTable: sql.Table("user"),
}
}
type sqlLibraryQuery struct {
sqltemplate.SQLTemplate
Query *LibraryPanelQuery
LibraryElementTable string
UserTable string
}
func (r sqlLibraryQuery) Validate() error {
return nil // TODO
}
func newLibraryQueryReq(sql *legacysql.LegacyDatabaseHelper, query *LibraryPanelQuery) sqlLibraryQuery {
return sqlLibraryQuery{
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
Query: query,
LibraryElementTable: sql.Table("library_element"),
UserTable: sql.Table("user"),
}
}

@ -23,6 +23,12 @@ func TestQueries(t *testing.T) {
return &v
}
getLibraryQuery := func(q *LibraryPanelQuery) sqltemplate.SQLTemplate {
v := newLibraryQueryReq(nodb, q)
v.SQLTemplate = mocks.NewTestingSQLTemplate()
return &v
}
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{
RootDir: "testdata",
Templates: map[*template.Template][]mocks.TemplateTestCase{
@ -64,6 +70,29 @@ func TestQueries(t *testing.T) {
}),
},
},
sqlQueryPanels: {
{
Name: "list",
Data: getLibraryQuery(&LibraryPanelQuery{
OrgID: 1,
Limit: 5,
}),
},
{
Name: "list_page_two",
Data: getLibraryQuery(&LibraryPanelQuery{
OrgID: 1,
LastID: 4,
}),
},
{
Name: "get_uid",
Data: getLibraryQuery(&LibraryPanelQuery{
OrgID: 1,
UID: "xyz",
}),
},
},
},
})
}

@ -0,0 +1,18 @@
SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM {{ .Ident .LibraryElementTable }} as p
LEFT OUTER JOIN {{ .Ident .UserTable }} AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN {{ .Ident .UserTable }} AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = {{ .Arg .Query.OrgID }}
{{ if .Query.LastID }}
AND p.id > {{ .Arg .Query.LastID }}
{{ end }}
{{ if .Query.UID }}
AND p.uid = {{ .Arg .Query.UID }}
{{ end }}
ORDER BY p.id DESC
{{ if .Query.Limit }}
LIMIT {{ .Arg .Query.Limit }}
{{ end }}

@ -6,10 +6,12 @@ import (
"encoding/json"
"fmt"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
@ -50,7 +52,8 @@ type dashboardSqlAccess struct {
provisioning provisioning.ProvisioningService
// Use for writing (not reading)
dashStore dashboards.Store
dashStore dashboards.Store
softDelete bool
// Typically one... the server wrapper
subscribers []chan *resource.WrittenEvent
@ -61,12 +64,14 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider,
namespacer request.NamespaceMapper,
dashStore dashboards.Store,
provisioning provisioning.ProvisioningService,
softDelete bool,
) DashboardAccess {
return &dashboardSqlAccess{
sql: sql,
namespacer: namespacer,
dashStore: dashStore,
provisioning: provisioning,
softDelete: softDelete,
}
}
@ -314,6 +319,16 @@ func (a *dashboardSqlAccess) DeleteDashboard(ctx context.Context, orgId int64, u
return nil, false, err
}
if a.softDelete {
err = a.dashStore.SoftDeleteDashboard(ctx, orgId, uid)
if err == nil && dash != nil {
now := metav1.NewTime(time.Now())
dash.DeletionTimestamp = &now
return dash, true, err
}
return dash, false, err
}
id := dash.Spec.GetNestedInt64("id")
if id == 0 {
return nil, false, fmt.Errorf("could not find id in saved body")
@ -383,3 +398,128 @@ func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, das
dash, _, err = a.GetDashboard(ctx, orgId, out.UID, 0)
return dash, created, err
}
func (a *dashboardSqlAccess) GetLibraryPanels(ctx context.Context, query LibraryPanelQuery) (*dashboardsV0.LibraryPanelList, error) {
limit := int(query.Limit)
query.Limit += 1 // for continue
if query.OrgID == 0 {
return nil, fmt.Errorf("expected non zero orgID")
}
sql, err := a.sql(ctx)
if err != nil {
return nil, err
}
req := newLibraryQueryReq(sql, &query)
rawQuery, err := sqltemplate.Execute(sqlQueryPanels, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlQueryPanels.Name(), err)
}
q := rawQuery
res := &dashboardsV0.LibraryPanelList{}
rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...)
defer func() {
if rows != nil {
_ = rows.Close()
}
}()
if err != nil {
return nil, err
}
type panel struct {
ID int64
UID string
FolderUID string
Created time.Time
CreatedBy string
Updated time.Time
UpdatedBy string
Name string
Type string
Description string
Model []byte
}
var lastID int64
for rows.Next() {
p := panel{}
err = rows.Scan(&p.ID, &p.UID, &p.FolderUID,
&p.Created, &p.CreatedBy,
&p.Updated, &p.UpdatedBy,
&p.Name, &p.Type, &p.Description, &p.Model,
)
if err != nil {
return res, err
}
lastID = p.ID
item := dashboardsV0.LibraryPanel{
ObjectMeta: metav1.ObjectMeta{
Name: p.UID,
CreationTimestamp: metav1.NewTime(p.Created),
ResourceVersion: strconv.FormatInt(p.Updated.UnixMilli(), 10),
},
Spec: dashboardsV0.LibraryPanelSpec{},
}
status := &dashboardsV0.LibraryPanelStatus{
Missing: v0alpha1.Unstructured{},
}
err = json.Unmarshal(p.Model, &item.Spec)
if err != nil {
return nil, err
}
err = json.Unmarshal(p.Model, &status.Missing.Object)
if err != nil {
return nil, err
}
if item.Spec.Title != p.Name {
status.Warnings = append(item.Status.Warnings, fmt.Sprintf("title mismatch (expected: %s)", p.Name))
}
if item.Spec.Description != p.Description {
status.Warnings = append(item.Status.Warnings, fmt.Sprintf("description mismatch (expected: %s)", p.Description))
}
if item.Spec.Type != p.Type {
status.Warnings = append(item.Status.Warnings, fmt.Sprintf("type mismatch (expected: %s)", p.Type))
}
item.Status = status
// Remove the properties we are already showing
for _, k := range []string{"type", "pluginVersion", "title", "description", "options", "fieldConfig", "datasource", "targets", "libraryPanel"} {
delete(status.Missing.Object, k)
}
meta, err := utils.MetaAccessor(&item)
if err != nil {
return nil, err
}
meta.SetFolder(p.FolderUID)
meta.SetCreatedBy(p.CreatedBy)
meta.SetUpdatedBy(p.UpdatedBy)
meta.SetUpdatedTimestamp(&p.Updated)
meta.SetOriginInfo(&utils.ResourceOriginInfo{
Name: "SQL",
Path: strconv.FormatInt(p.ID, 10),
})
res.Items = append(res.Items, item)
if len(res.Items) > limit {
res.Continue = strconv.FormatInt(lastID, 10)
break
}
}
if query.UID == "" {
rv, err := sql.GetResourceVersion(ctx, "library_element", "updated")
if err == nil {
res.ResourceVersion = strconv.FormatInt(rv, 10)
}
}
return res, err
}

@ -0,0 +1,10 @@
SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
AND p.uid = 'xyz'
ORDER BY p.id DESC

@ -0,0 +1,10 @@
SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
ORDER BY p.id DESC
LIMIT 5

@ -0,0 +1,10 @@
SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
AND p.id > 4
ORDER BY p.id DESC

@ -0,0 +1,10 @@
SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
AND p.uid = 'xyz'
ORDER BY p.id DESC

@ -0,0 +1,10 @@
SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
ORDER BY p.id DESC
LIMIT 5

@ -0,0 +1,10 @@
SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
AND p.id > 4
ORDER BY p.id DESC

@ -0,0 +1,10 @@
SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
AND p.uid = 'xyz'
ORDER BY p.id DESC

@ -0,0 +1,10 @@
SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
ORDER BY p.id DESC
LIMIT 5

@ -0,0 +1,10 @@
SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
AND p.id > 4
ORDER BY p.id DESC

@ -33,6 +33,16 @@ func (r *DashboardQuery) UseHistoryTable() bool {
return r.GetHistory || r.Version > 0
}
type LibraryPanelQuery struct {
OrgID int64
UID string // to select a single dashboard
Limit int64
// Included in the continue token
// This is the ID from the last dashboard sent in the previous page
LastID int64
}
type DashboardAccess interface {
resource.StorageBackend
resource.ResourceIndexServer
@ -40,4 +50,7 @@ type DashboardAccess interface {
GetDashboard(ctx context.Context, orgId int64, uid string, version int64) (*dashboardsV0.Dashboard, int64, 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)
// Get a typed list
GetLibraryPanels(ctx context.Context, query LibraryPanelQuery) (*dashboardsV0.LibraryPanelList, error)
}

@ -0,0 +1,94 @@
package dashboard
import (
"context"
"fmt"
"strconv"
"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"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
)
var (
_ rest.Scoper = (*libraryPanelStore)(nil)
_ rest.SingularNameProvider = (*libraryPanelStore)(nil)
_ rest.Getter = (*libraryPanelStore)(nil)
_ rest.Lister = (*libraryPanelStore)(nil)
_ rest.Storage = (*libraryPanelStore)(nil)
)
var lpr = dashboard.LibraryPanelResourceInfo
type libraryPanelStore struct {
access legacy.DashboardAccess
}
func (s *libraryPanelStore) New() runtime.Object {
return lpr.NewFunc()
}
func (s *libraryPanelStore) Destroy() {}
func (s *libraryPanelStore) NamespaceScoped() bool {
return true // namespace == org
}
func (s *libraryPanelStore) GetSingularName() string {
return lpr.GetSingularName()
}
func (s *libraryPanelStore) NewList() runtime.Object {
return lpr.NewListFunc()
}
func (s *libraryPanelStore) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return lpr.TableConverter().ConvertToTable(ctx, object, tableOptions)
}
func (s *libraryPanelStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
query := legacy.LibraryPanelQuery{
OrgID: ns.OrgID,
Limit: options.Limit,
}
if options.Continue != "" {
query.LastID, err = strconv.ParseInt(options.Continue, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid continue token")
}
}
if query.Limit < 1 {
query.Limit = 25
}
return s.access.GetLibraryPanels(ctx, query)
}
func (s *libraryPanelStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
query := legacy.LibraryPanelQuery{
OrgID: ns.OrgID,
UID: name,
Limit: 1,
}
found, err := s.access.GetLibraryPanels(ctx, query)
if err != nil {
return nil, err
}
if len(found.Items) == 1 {
return &found.Items[0], nil
}
return nil, lpr.NewNotFound(name)
}

@ -56,6 +56,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
return nil // skip registration unless opting into experimental apis
}
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
dbp := legacysql.NewDatabaseProvider(sql)
namespacer := request.GetNamespaceMapper(cfg)
builder := &DashboardsAPIBuilder{
@ -66,7 +67,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
legacy: &dashboardStorage{
resource: dashboard.DashboardResourceInfo,
access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning),
access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete),
tableConverter: dashboard.DashboardResourceInfo.TableConverter(),
},
}
@ -90,6 +91,8 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
&dashboard.DashboardWithAccessInfo{},
&dashboard.DashboardVersionList{},
&dashboard.VersionsQueryOptions{},
&dashboard.LibraryPanel{},
&dashboard.LibraryPanelList{},
&metav1.PartialObjectMetadata{},
&metav1.PartialObjectMetadataList{},
)
@ -156,6 +159,11 @@ func (b *DashboardsAPIBuilder) GetAPIGroupInfo(
}
}
// Expose read only library panels
storage[dashboard.LibraryPanelResourceInfo.StoragePath()] = &libraryPanelStore{
access: b.legacy.access,
}
apiGroupInfo.VersionedResourcesStorageMap[dashboard.VERSION] = storage
return &apiGroupInfo, nil
}

@ -4,7 +4,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
)
@ -17,7 +17,7 @@ type storage struct {
func newStorage(scheme *runtime.Scheme) (*storage, error) {
strategy := grafanaregistry.NewStrategy(scheme)
resourceInfo := v0alpha1.DashboardResourceInfo
resourceInfo := dashboard.DashboardResourceInfo
store := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,

@ -17,6 +17,17 @@ func GetOpenAPIDefinitions(builders []APIGroupBuilder) common.GetOpenAPIDefiniti
return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
defs := v0alpha1.GetOpenAPIDefinitions(ref) // common grafana apis
maps.Copy(defs, data.GetOpenAPIDefinitions(ref))
// TODO: remove when https://github.com/grafana/grafana-plugin-sdk-go/pull/1062 is merged
maps.Copy(defs, map[string]common.OpenAPIDefinition{
"github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataSourceRef": {
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{Allows: true},
},
},
},
})
for _, b := range builders {
g := b.GetOpenAPIDefinitions()
if g != nil {

@ -159,6 +159,20 @@ func TestIntegrationDashboardsApp(t *testing.T) {
"update",
"watch"
]
},
{
"resource": "librarypanels",
"responseKind": {
"group": "",
"kind": "LibraryPanel",
"version": ""
},
"scope": "Namespaced",
"singularResource": "librarypanel",
"verbs": [
"get",
"list"
]
}
],
"version": "v0alpha1"

Loading…
Cancel
Save