K8s/Dashboards: Fix dashboard list and add tests (#91523)

This commit is contained in:
Ryan McKinley 2024-08-07 13:43:13 +03:00 committed by GitHub
parent e8d5d5fbff
commit 9e116d13a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 740 additions and 138 deletions

View File

@ -0,0 +1,38 @@
package legacy
import (
"embed"
"fmt"
"text/template"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
// Templates setup.
var (
//go:embed *.sql
sqlTemplatesFS embed.FS
sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `*.sql`))
)
func mustTemplate(filename string) *template.Template {
if t := sqlTemplates.Lookup(filename); t != nil {
return t
}
panic(fmt.Sprintf("template file not found: %s", filename))
}
// Templates.
var (
sqlQueryDashboards = mustTemplate("query_dashboards.sql")
)
type sqlQuery struct {
*sqltemplate.SQLTemplate
Query *DashboardQuery
}
func (r sqlQuery) Validate() error {
return nil // TODO
}

View File

@ -0,0 +1,167 @@
package legacy
import (
"embed"
"os"
"path/filepath"
"testing"
"text/template"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
//go:embed testdata/*
var testdataFS embed.FS
func testdata(t *testing.T, filename string) []byte {
t.Helper()
b, err := testdataFS.ReadFile(`testdata/` + filename)
if err != nil {
writeTestData(filename, "<empty>")
assert.Fail(t, "missing test file")
}
return b
}
func writeTestData(filename, value string) {
_ = os.WriteFile(filepath.Join("testdata", filename), []byte(value), 0777)
}
func TestQueries(t *testing.T) {
t.Parallel()
// Check each dialect
dialects := []sqltemplate.Dialect{
sqltemplate.MySQL,
sqltemplate.SQLite,
sqltemplate.PostgreSQL,
}
// Each template has one or more test cases, each identified with a
// descriptive name (e.g. "happy path", "error twiddling the frobb"). Each
// of them will test that for the same input data they must produce a result
// that will depend on the Dialect. Expected queries should be defined in
// separate files in the testdata directory. This improves the testing
// experience by separating test data from test code, since mixing both
// tends to make it more difficult to reason about what is being done,
// especially as we want testing code to scale and make it easy to add
// tests.
type (
testCase = struct {
Name string
// Data should be the struct passed to the template.
Data sqltemplate.SQLTemplateIface
}
)
// Define tests cases. Most templates are trivial and testing that they
// generate correct code for a single Dialect is fine, since the one thing
// that always changes is how SQL placeholder arguments are passed (most
// Dialects use `?` while PostgreSQL uses `$1`, `$2`, etc.), and that is
// something that should be tested in the Dialect implementation instead of
// here. We will ask to have at least one test per SQL template, and we will
// lean to test MySQL. Templates containing branching (conditionals, loops,
// etc.) should be exercised at least once in each of their branches.
//
// NOTE: in the Data field, make sure to have pointers populated to simulate
// data is set as it would be in a real request. The data being correctly
// populated in each case should be tested in integration tests, where the
// data will actually flow to and from a real database. In this tests we
// only care about producing the correct SQL.
testCases := map[*template.Template][]*testCase{
sqlQueryDashboards: {
{
Name: "history_uid",
Data: &sqlQuery{
SQLTemplate: new(sqltemplate.SQLTemplate),
Query: &DashboardQuery{
OrgID: 2,
UID: "UUU",
},
},
},
{
Name: "history_uid_at_version",
Data: &sqlQuery{
SQLTemplate: new(sqltemplate.SQLTemplate),
Query: &DashboardQuery{
OrgID: 2,
UID: "UUU",
Version: 3,
},
},
},
{
Name: "history_uid_second_page",
Data: &sqlQuery{
SQLTemplate: new(sqltemplate.SQLTemplate),
Query: &DashboardQuery{
OrgID: 2,
UID: "UUU",
LastID: 7,
},
},
},
{
Name: "dashboard",
Data: &sqlQuery{
SQLTemplate: new(sqltemplate.SQLTemplate),
Query: &DashboardQuery{
OrgID: 2,
},
},
},
{
Name: "dashboard_next_page",
Data: &sqlQuery{
SQLTemplate: new(sqltemplate.SQLTemplate),
Query: &DashboardQuery{
OrgID: 2,
LastID: 22,
},
},
},
},
}
// Execute test cases
for tmpl, tcs := range testCases {
t.Run(tmpl.Name(), func(t *testing.T) {
t.Parallel()
for _, tc := range tcs {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
for _, dialect := range dialects {
filename := dialect.DialectName() + "__" + tc.Name + ".sql"
t.Run(filename, func(t *testing.T) {
// not parallel because we're sharing tc.Data, not
// worth it deep cloning
expectedQuery := string(testdata(t, filename))
//expectedQuery := sqltemplate.FormatSQL(rawQuery)
tc.Data.SetDialect(dialect)
err := tc.Data.Validate()
require.NoError(t, err)
got, err := sqltemplate.Execute(tmpl, tc.Data)
require.NoError(t, err)
got = sqltemplate.RemoveEmptyLines(got)
if diff := cmp.Diff(expectedQuery, got); diff != "" {
writeTestData(filename, got)
t.Errorf("%s: %s", tc.Name, diff)
}
})
}
})
}
})
}
}

View File

@ -0,0 +1,45 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
{{ if .Query.UseHistoryTable }}
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message, dashboard_version.data
{{ else }}
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
{{ end }}
FROM dashboard
{{ if .Query.UseHistoryTable }}
LEFT OUTER JOIN dashboard_version ON dashboard.id = dashboard_version.dashboard_id
{{ end }}
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN {{ .Ident "user" }} AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN {{ .Ident "user" }} AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = {{ .Arg .Query.OrgID }}
{{ if .Query.UseHistoryTable }}
{{ if .Query.Version }}
AND dashboard_version.version = {{ .Arg .Query.Version }}
{{ else if .Query.LastID }}
AND dashboard_version.version < {{ .Arg .Query.LastID }}
{{ end }}
ORDER BY dashboard_version.version DESC
{{ else }}
{{ if .Query.UID }}
AND dashboard.uid = {{ .Arg .Query.UID }}
{{ else if .Query.LastID }}
AND dashboard.id > {{ .Arg .Query.LastID }}
{{ end }}
{{ if .Query.GetTrash }}
AND dashboard.deleted IS NOT NULL
{{ else if .Query.LastID }}
AND dashboard.deleted IS NULL
{{ end }}
ORDER BY dashboard.id DESC
{{ end }}

View File

@ -9,9 +9,6 @@ import (
"sync" "sync"
"time" "time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
"github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
@ -21,8 +18,12 @@ import (
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/session" "github.com/grafana/grafana/pkg/services/sqlstore/session"
"github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
) )
var ( var (
@ -46,10 +47,12 @@ type dashboardRow struct {
type dashboardSqlAccess struct { type dashboardSqlAccess struct {
sql db.DB sql db.DB
dialect sqltemplate.Dialect
sess *session.SessionDB sess *session.SessionDB
namespacer request.NamespaceMapper namespacer request.NamespaceMapper
dashStore dashboards.Store dashStore dashboards.Store
provisioning provisioning.ProvisioningService provisioning provisioning.ProvisioningService
currentRV func(ctx context.Context) (int64, error)
// Typically one... the server wrapper // Typically one... the server wrapper
subscribers []chan *resource.WrittenEvent subscribers []chan *resource.WrittenEvent
@ -61,131 +64,82 @@ func NewDashboardAccess(sql db.DB,
dashStore dashboards.Store, dashStore dashboards.Store,
provisioning provisioning.ProvisioningService, provisioning provisioning.ProvisioningService,
) DashboardAccess { ) DashboardAccess {
dialect := sqltemplate.DialectForDriver(string(sql.GetDBType()))
if dialect == nil {
// panic?
// fmt.Errorf("no dialect for driver %q", driverName)
fmt.Printf("ERROR: NO DIALECT")
}
sess := sql.GetSqlxSession()
currentRV := func(ctx context.Context) (int64, error) {
t := time.Now()
max := ""
err := sess.Get(ctx, &max, "SELECT MAX(updated) FROM dashboard")
if err == nil && max != "" {
t, _ = time.Parse(time.DateTime, max) // ignore null errors
}
return t.UnixMilli(), nil
}
if sql.GetDBType() == migrator.Postgres {
currentRV = func(ctx context.Context) (int64, error) {
max := time.Now()
_ = sess.Get(ctx, &max, "SELECT MAX(updated) FROM dashboard")
return max.UnixMilli(), nil
}
} else if sql.GetDBType() == migrator.MySQL {
currentRV = func(ctx context.Context) (int64, error) {
max := time.Now().UnixMilli()
_ = sess.Get(ctx, &max, "SELECT UNIX_TIMESTAMP(MAX(updated)) FROM dashboard;")
return max, nil
}
}
return &dashboardSqlAccess{ return &dashboardSqlAccess{
sql: sql, sql: sql,
sess: sql.GetSqlxSession(), sess: sess,
dialect: dialect,
namespacer: namespacer, namespacer: namespacer,
dashStore: dashStore, dashStore: dashStore,
provisioning: provisioning, provisioning: provisioning,
currentRV: currentRV,
} }
} }
func (a *dashboardSqlAccess) currentRV(ctx context.Context) (int64, error) {
t := time.Now()
max := ""
err := a.sess.Get(ctx, &max, "SELECT MAX(updated) FROM dashboard")
if err == nil && max != "" {
t, err = time.Parse(time.DateTime, max)
}
return t.UnixMilli(), err
}
const selector = `SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.created,CreatedUSER.uid as created_by,
dashboard.updated,UpdatedUSER.uid as updated_by,
dashboard.deleted, 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, '', 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.updated_by = UpdatedUSER.id
WHERE dashboard.is_folder = false`
const history = `SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.created,CreatedUSER.uid as created_by,
dashboard_version.created,UpdatedUSER.uid as updated_by,
NULL, 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.version, dashboard_version.message, dashboard_version.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN user AS CreatedUSER ON dashboard.created_by = CreatedUSER.id
LEFT OUTER JOIN user AS UpdatedUSER ON dashboard_version.created_by = UpdatedUSER.id
WHERE dashboard.is_folder = false`
func (a *dashboardSqlAccess) getRows(ctx context.Context, query *DashboardQuery) (*rowsWrapper, error) { func (a *dashboardSqlAccess) getRows(ctx context.Context, query *DashboardQuery) (*rowsWrapper, error) {
if len(query.Labels) > 0 { if len(query.Labels) > 0 {
return nil, fmt.Errorf("labels not yet supported") return nil, fmt.Errorf("labels not yet supported")
// if query.Requirements.Folder != nil { // if query.Requirements.Folder != nil {
// args = append(args, *query.Requirements.Folder) // args = append(args, *query.Requirements.Folder)
// sqlcmd = fmt.Sprintf("%s AND dashboard.folder_uid=$%d", sqlcmd, len(args)) // sqlcmd = fmt.Sprintf("%s AND dashboard.folder_uid=?$%d", sqlcmd, len(args))
// } // }
} }
var sqlcmd string req := sqlQuery{
args := []any{query.OrgID} SQLTemplate: sqltemplate.New(a.dialect),
Query: query,
if query.GetHistory || query.Version > 0 {
if query.GetTrash {
return nil, fmt.Errorf("trash not included in history table")
}
sqlcmd = fmt.Sprintf("%s AND dashboard.org_id=$%d\n ", history, len(args))
if query.UID == "" {
return nil, fmt.Errorf("history query must have a UID")
}
args = append(args, query.UID)
sqlcmd = fmt.Sprintf("%s AND dashboard.uid=$%d", sqlcmd, len(args))
if query.Version > 0 {
args = append(args, query.Version)
sqlcmd = fmt.Sprintf("%s AND dashboard_version.version=$%d", sqlcmd, len(args))
} else if query.LastID > 0 {
args = append(args, query.LastID)
sqlcmd = fmt.Sprintf("%s AND dashboard_version.version<$%d", sqlcmd, len(args))
}
sqlcmd = fmt.Sprintf("%s\n ORDER BY dashboard_version.version desc", sqlcmd)
} else {
sqlcmd = fmt.Sprintf("%s AND dashboard.org_id=$%d\n ", selector, len(args))
if query.UID != "" {
args = append(args, query.UID)
sqlcmd = fmt.Sprintf("%s AND dashboard.uid=$%d", sqlcmd, len(args))
} else if query.LastID > 0 {
args = append(args, query.LastID)
sqlcmd = fmt.Sprintf("%s AND dashboard.id>$%d", sqlcmd, len(args))
}
if query.GetTrash {
sqlcmd = sqlcmd + " AND dashboard.deleted IS NOT NULL"
} else {
sqlcmd = sqlcmd + " AND dashboard.deleted IS NULL"
}
sqlcmd = fmt.Sprintf("%s\n ORDER BY dashboard.id asc", sqlcmd)
} }
// fmt.Printf("%s // %v\n", sqlcmd, args)
rows, err := a.doQuery(ctx, sqlcmd, args...) tmpl := sqlQueryDashboards
if query.UseHistoryTable() && query.GetTrash {
return nil, fmt.Errorf("trash not included in history table")
}
rawQuery, err := sqltemplate.Execute(tmpl, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", tmpl.Name(), err)
}
q := rawQuery
// q = sqltemplate.RemoveEmptyLines(rawQuery)
// fmt.Printf(">>%s [%+v]", q, req.GetArgs())
rows, err := a.sess.Query(ctx, q, req.GetArgs()...)
if err != nil { if err != nil {
if rows != nil { if rows != nil {
_ = rows.Close() _ = rows.Close()
} }
rows = nil rows = nil
} }
return rows, err
}
func (a *dashboardSqlAccess) doQuery(ctx context.Context, query string, args ...any) (*rowsWrapper, error) {
_, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
rows, err := a.sess.Query(ctx, query, args...)
return &rowsWrapper{ return &rowsWrapper{
rows: rows, rows: rows,
a: a, a: a,
@ -211,6 +165,9 @@ type rowsWrapper struct {
} }
func (r *rowsWrapper) Close() error { func (r *rowsWrapper) Close() error {
if r.rows == nil {
return nil
}
return r.rows.Close() return r.rows.Close()
} }
@ -291,10 +248,12 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
var folder_uid sql.NullString var folder_uid sql.NullString
var updated time.Time var updated time.Time
var updatedBy sql.NullString var updatedBy sql.NullString
var updatedByID sql.NullInt64
var deleted sql.NullTime var deleted sql.NullTime
var created time.Time var created time.Time
var createdBy sql.NullString var createdBy sql.NullString
var createdByID sql.NullInt64
var message sql.NullString var message sql.NullString
var plugin_id string var plugin_id string
@ -306,10 +265,10 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
var version int64 var version int64
err := rows.Scan(&orgId, &dashboard_id, &dash.Name, &folder_uid, err := rows.Scan(&orgId, &dashboard_id, &dash.Name, &folder_uid,
&created, &createdBy,
&updated, &updatedBy,
&deleted, &plugin_id, &deleted, &plugin_id,
&origin_name, &origin_path, &origin_hash, &origin_ts, &origin_name, &origin_path, &origin_hash, &origin_ts,
&created, &createdBy, &createdByID,
&updated, &updatedBy, &updatedByID,
&version, &message, &data, &version, &message, &data,
) )
@ -325,8 +284,8 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
return nil, err return nil, err
} }
meta.SetUpdatedTimestamp(&updated) meta.SetUpdatedTimestamp(&updated)
meta.SetCreatedBy(getUserID(createdBy)) meta.SetCreatedBy(getUserID(createdBy, createdByID))
meta.SetUpdatedBy(getUserID(updatedBy)) meta.SetUpdatedBy(getUserID(updatedBy, updatedByID))
if deleted.Valid { if deleted.Valid {
meta.SetDeletionTimestamp(ptr.To(metav1.NewTime(deleted.Time))) meta.SetDeletionTimestamp(ptr.To(metav1.NewTime(deleted.Time)))
@ -377,11 +336,14 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
return row, err return row, err
} }
func getUserID(v sql.NullString) string { func getUserID(v sql.NullString, id sql.NullInt64) string {
if v.String == "" { if v.Valid && v.String != "" {
return identity.NewTypedIDString(identity.TypeUser, v.String).String()
}
if id.Valid && id.Int64 == -1 {
return identity.NewTypedIDString(identity.TypeProvisioning, "").String() return identity.NewTypedIDString(identity.TypeProvisioning, "").String()
} }
return identity.NewTypedIDString(identity.TypeUser, v.String).String() return ""
} }
// DeleteDashboard implements DashboardAccess. // DeleteDashboard implements DashboardAccess.

View File

@ -147,13 +147,13 @@ func (a *dashboardSqlAccess) ReadResource(ctx context.Context, req *resource.Rea
rsp.Error = &resource.ErrorResult{ rsp.Error = &resource.ErrorResult{
Code: http.StatusNotFound, Code: http.StatusNotFound,
} }
} else {
rsp.Value, err = json.Marshal(dash)
if err != nil {
rsp.Error = resource.AsErrorResult(err)
}
} }
rsp.ResourceVersion = rv rsp.ResourceVersion = rv
rsp.Value, err = json.Marshal(dash)
if err != nil {
rsp.Error = resource.AsErrorResult(err)
}
return rsp return rsp
} }
@ -177,11 +177,10 @@ func (a *dashboardSqlAccess) ListIterator(ctx context.Context, req *resource.Lis
} }
query := &DashboardQuery{ query := &DashboardQuery{
OrgID: info.OrgID, OrgID: info.OrgID,
Limit: int(req.Limit), Limit: int(req.Limit),
MaxBytes: 2 * 1024 * 1024, // 2MB, LastID: token.id,
LastID: token.id, Labels: req.Options.Labels,
Labels: req.Options.Labels,
} }
listRV, err := a.currentRV(ctx) listRV, err := a.currentRV(ctx)
@ -194,7 +193,7 @@ func (a *dashboardSqlAccess) ListIterator(ctx context.Context, req *resource.Lis
_ = rows.Close() _ = rows.Close()
}() }()
} }
if err != nil { if err == nil {
err = cb(rows) err = cb(rows)
} }
return listRV, err return listRV, err
@ -253,13 +252,15 @@ func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryR
if token.orgId > 0 && token.orgId != info.OrgID { if token.orgId > 0 && token.orgId != info.OrgID {
return nil, fmt.Errorf("token and orgID mismatch") return nil, fmt.Errorf("token and orgID mismatch")
} }
limit := int(req.Limit)
if limit < 1 {
limit = 15
}
query := &DashboardQuery{ query := &DashboardQuery{
OrgID: info.OrgID, OrgID: info.OrgID,
Limit: int(req.Limit), Limit: limit + 1,
MaxBytes: 2 * 1024 * 1024, // 2MB, LastID: token.id,
LastID: token.id, UID: req.Key.Name,
UID: req.Key.Name,
} }
if req.ShowDeleted { if req.ShowDeleted {
query.GetTrash = true query.GetTrash = true
@ -273,7 +274,6 @@ func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryR
} }
defer func() { _ = rows.Close() }() defer func() { _ = rows.Close() }()
totalSize := 0
list := &resource.HistoryResponse{} list := &resource.HistoryResponse{}
for rows.Next() { for rows.Next() {
if rows.err != nil || rows.row == nil { if rows.err != nil || rows.row == nil {
@ -291,8 +291,7 @@ func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryR
return list, err return list, err
} }
totalSize += len(rows.Value()) if len(list.Items) >= limit {
if len(list.Items) > 0 && (totalSize > query.MaxBytes || len(list.Items) >= query.Limit) {
// if query.Requirements.Folder != nil { // if query.Requirements.Folder != nil {
// row.token.folder = *query.Requirements.Folder // row.token.folder = *query.Requirements.Folder
// } // }

View File

@ -0,0 +1,18 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN `user` AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `user` AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN `user` AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `user` AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
AND dashboard.id > ?
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,19 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN `user` AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `user` AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
AND dashboard.uid = ?
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message, dashboard_version.data
FROM dashboard
LEFT OUTER JOIN dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN `user` AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `user` AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
AND dashboard_version.version = ?
ORDER BY dashboard_version.version DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN `user` AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `user` AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
AND dashboard.uid = ?
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,18 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = $1
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = $1
AND dashboard.id > $2
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,19 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = $1
AND dashboard.uid = $2
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message, dashboard_version.data
FROM dashboard
LEFT OUTER JOIN dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = $1
AND dashboard_version.version = $2
ORDER BY dashboard_version.version DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = $1
AND dashboard.uid = $2
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,18 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
AND dashboard.id > ?
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,19 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
AND dashboard.uid = ?
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message, dashboard_version.data
FROM dashboard
LEFT OUTER JOIN dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
AND dashboard_version.version = ?
ORDER BY dashboard_version.version DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, 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.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
AND dashboard.uid = ?
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC

View File

@ -10,10 +10,9 @@ import (
// This does not check if you have permissions! // This does not check if you have permissions!
type DashboardQuery struct { type DashboardQuery struct {
OrgID int64 OrgID int64
UID string // to select a single dashboard UID string // to select a single dashboard
Limit int Limit int
MaxBytes int
// Included in the continue token // Included in the continue token
// This is the ID from the last dashboard sent in the previous page // This is the ID from the last dashboard sent in the previous page
@ -30,6 +29,10 @@ type DashboardQuery struct {
Labels []*resource.Requirement Labels []*resource.Requirement
} }
func (r *DashboardQuery) UseHistoryTable() bool {
return r.GetHistory || r.Version > 0
}
type DashboardAccess interface { type DashboardAccess interface {
resource.StorageBackend resource.StorageBackend
resource.ResourceIndexServer resource.ResourceIndexServer

View File

@ -14,8 +14,25 @@ var MySQL = mysql{
var _ Dialect = MySQL var _ Dialect = MySQL
type mysql struct { type mysql struct {
standardIdent backtickIdent
rowLockingClauseMap rowLockingClauseMap
argPlaceholderFunc argPlaceholderFunc
name name
} }
// standardIdent provides standard SQL escaping of identifiers.
type backtickIdent struct{}
var standardFallback = standardIdent{}
func (backtickIdent) Ident(s string) (string, error) {
switch s {
// Internal identifiers require backticks to work properly
case "user":
return "`" + s + "`", nil
case "":
return "", ErrEmptyIdent
}
// standard
return standardFallback.Ident(s)
}

View File

@ -116,6 +116,22 @@ func FormatSQL(q string) string {
return q return q
} }
// RemoveEmptyLines removes the empty lines from a SQL statement
// empty lines are typical when using text template formatting
func RemoveEmptyLines(q string) string {
var b strings.Builder
lines := strings.Split(strings.ReplaceAll(q, "\r\n", "\n"), "\n")
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
line = strings.ReplaceAll(line, "\t", " ")
b.WriteString(line)
b.WriteByte('\n')
}
return b.String()
}
type reFormatting struct { type reFormatting struct {
re *regexp.Regexp re *regexp.Regexp
replacement string replacement string

View File

@ -1,16 +1,26 @@
package dashboards package dashboards
import ( import (
"context"
"strings"
"testing" "testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
) )
var gvr = schema.GroupVersionResource{
Group: "dashboard.grafana.app",
Version: "v0alpha1",
Resource: "dashboards",
}
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
testsuite.Run(m) testsuite.Run(m)
} }
@ -45,6 +55,59 @@ func TestIntegrationDashboardsApp(t *testing.T) {
_, err := helper.NewDiscoveryClient().ServerResourcesForGroupVersion("dashboard.grafana.app/v0alpha1") _, err := helper.NewDiscoveryClient().ServerResourcesForGroupVersion("dashboard.grafana.app/v0alpha1")
require.NoError(t, err) require.NoError(t, err)
t.Run("simple crud+list", func(t *testing.T) {
ctx := context.Background()
client := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
GVR: gvr,
})
rsp, err := client.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.Empty(t, rsp.Items)
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]any{
"title": "Test empty dashboard",
},
},
}
obj.SetGenerateName("aa")
obj, err = client.Resource.Create(ctx, obj, metav1.CreateOptions{})
require.NoError(t, err)
created := obj.GetName()
require.True(t, strings.HasPrefix(created, "aa"), "expecting prefix %s (%s)", "aa", created) // the generate name prefix
// The new value exists in a list
rsp, err = client.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.Len(t, rsp.Items, 1)
require.Equal(t, created, rsp.Items[0].GetName())
// Same value returned from get
obj, err = client.Resource.Get(ctx, created, metav1.GetOptions{})
require.NoError(t, err)
require.Equal(t, created, obj.GetName())
// Commented out because the dynamic client does not like lists as sub-resource
// // Check that it now appears in the history
// sub, err := client.Resource.Get(ctx, created, metav1.GetOptions{}, "history")
// require.NoError(t, err)
// history, err := sub.ToList()
// require.NoError(t, err)
// require.Len(t, history.Items, 1)
// require.Equal(t, created, history.Items[0].GetName())
// Delete the object
err = client.Resource.Delete(ctx, created, metav1.DeleteOptions{})
require.NoError(t, err)
// Now it is not in the list
rsp, err = client.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.Empty(t, rsp.Items)
})
t.Run("Check discovery client", func(t *testing.T) { t.Run("Check discovery client", func(t *testing.T) {
disco := helper.GetGroupVersionInfoJSON("dashboard.grafana.app") disco := helper.GetGroupVersionInfoJSON("dashboard.grafana.app")
// fmt.Printf("%s", string(disco)) // fmt.Printf("%s", string(disco))

View File

@ -437,6 +437,7 @@ func (c *K8sTestHelper) CreateUser(name string, orgName string, basicRole org.Ro
Password: user.Password(name), Password: user.Password(name),
Login: fmt.Sprintf("%s-%d", name, orgId), Login: fmt.Sprintf("%s-%d", name, orgId),
OrgID: orgId, OrgID: orgId,
IsAdmin: basicRole == identity.RoleAdmin && orgId == 1, // make org1 admins grafana admins
}) })
require.NoError(c.t, err) require.NoError(c.t, err)
require.Equal(c.t, orgId, u.OrgID) require.Equal(c.t, orgId, u.OrgID)