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"
"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/utils"
dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
@ -21,8 +18,12 @@ import (
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboards"
"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/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 (
@ -46,10 +47,12 @@ type dashboardRow struct {
type dashboardSqlAccess struct {
sql db.DB
dialect sqltemplate.Dialect
sess *session.SessionDB
namespacer request.NamespaceMapper
dashStore dashboards.Store
provisioning provisioning.ProvisioningService
currentRV func(ctx context.Context) (int64, error)
// Typically one... the server wrapper
subscribers []chan *resource.WrittenEvent
@ -61,131 +64,82 @@ func NewDashboardAccess(sql db.DB,
dashStore dashboards.Store,
provisioning provisioning.ProvisioningService,
) 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{
sql: sql,
sess: sql.GetSqlxSession(),
sess: sess,
dialect: dialect,
namespacer: namespacer,
dashStore: dashStore,
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) {
if len(query.Labels) > 0 {
return nil, fmt.Errorf("labels not yet supported")
// if query.Requirements.Folder != nil {
// 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
args := []any{query.OrgID}
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)
req := sqlQuery{
SQLTemplate: sqltemplate.New(a.dialect),
Query: query,
}
// 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 rows != nil {
_ = rows.Close()
}
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{
rows: rows,
a: a,
@ -211,6 +165,9 @@ type rowsWrapper struct {
}
func (r *rowsWrapper) Close() error {
if r.rows == nil {
return nil
}
return r.rows.Close()
}
@ -291,10 +248,12 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
var folder_uid sql.NullString
var updated time.Time
var updatedBy sql.NullString
var updatedByID sql.NullInt64
var deleted sql.NullTime
var created time.Time
var createdBy sql.NullString
var createdByID sql.NullInt64
var message sql.NullString
var plugin_id string
@ -306,10 +265,10 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
var version int64
err := rows.Scan(&orgId, &dashboard_id, &dash.Name, &folder_uid,
&created, &createdBy,
&updated, &updatedBy,
&deleted, &plugin_id,
&origin_name, &origin_path, &origin_hash, &origin_ts,
&created, &createdBy, &createdByID,
&updated, &updatedBy, &updatedByID,
&version, &message, &data,
)
@ -325,8 +284,8 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
return nil, err
}
meta.SetUpdatedTimestamp(&updated)
meta.SetCreatedBy(getUserID(createdBy))
meta.SetUpdatedBy(getUserID(updatedBy))
meta.SetCreatedBy(getUserID(createdBy, createdByID))
meta.SetUpdatedBy(getUserID(updatedBy, updatedByID))
if deleted.Valid {
meta.SetDeletionTimestamp(ptr.To(metav1.NewTime(deleted.Time)))
@ -377,11 +336,14 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
return row, err
}
func getUserID(v sql.NullString) string {
if v.String == "" {
func getUserID(v sql.NullString, id sql.NullInt64) 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.TypeUser, v.String).String()
return ""
}
// DeleteDashboard implements DashboardAccess.

View File

@ -147,13 +147,13 @@ func (a *dashboardSqlAccess) ReadResource(ctx context.Context, req *resource.Rea
rsp.Error = &resource.ErrorResult{
Code: http.StatusNotFound,
}
} else {
rsp.Value, err = json.Marshal(dash)
if err != nil {
rsp.Error = resource.AsErrorResult(err)
}
}
rsp.ResourceVersion = rv
rsp.Value, err = json.Marshal(dash)
if err != nil {
rsp.Error = resource.AsErrorResult(err)
}
return rsp
}
@ -177,11 +177,10 @@ func (a *dashboardSqlAccess) ListIterator(ctx context.Context, req *resource.Lis
}
query := &DashboardQuery{
OrgID: info.OrgID,
Limit: int(req.Limit),
MaxBytes: 2 * 1024 * 1024, // 2MB,
LastID: token.id,
Labels: req.Options.Labels,
OrgID: info.OrgID,
Limit: int(req.Limit),
LastID: token.id,
Labels: req.Options.Labels,
}
listRV, err := a.currentRV(ctx)
@ -194,7 +193,7 @@ func (a *dashboardSqlAccess) ListIterator(ctx context.Context, req *resource.Lis
_ = rows.Close()
}()
}
if err != nil {
if err == nil {
err = cb(rows)
}
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 {
return nil, fmt.Errorf("token and orgID mismatch")
}
limit := int(req.Limit)
if limit < 1 {
limit = 15
}
query := &DashboardQuery{
OrgID: info.OrgID,
Limit: int(req.Limit),
MaxBytes: 2 * 1024 * 1024, // 2MB,
LastID: token.id,
UID: req.Key.Name,
OrgID: info.OrgID,
Limit: limit + 1,
LastID: token.id,
UID: req.Key.Name,
}
if req.ShowDeleted {
query.GetTrash = true
@ -273,7 +274,6 @@ func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryR
}
defer func() { _ = rows.Close() }()
totalSize := 0
list := &resource.HistoryResponse{}
for rows.Next() {
if rows.err != nil || rows.row == nil {
@ -291,8 +291,7 @@ func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryR
return list, err
}
totalSize += len(rows.Value())
if len(list.Items) > 0 && (totalSize > query.MaxBytes || len(list.Items) >= query.Limit) {
if len(list.Items) >= limit {
// if query.Requirements.Folder != nil {
// 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!
type DashboardQuery struct {
OrgID int64
UID string // to select a single dashboard
Limit int
MaxBytes int
OrgID int64
UID string // to select a single dashboard
Limit int
// Included in the continue token
// This is the ID from the last dashboard sent in the previous page
@ -30,6 +29,10 @@ type DashboardQuery struct {
Labels []*resource.Requirement
}
func (r *DashboardQuery) UseHistoryTable() bool {
return r.GetHistory || r.Version > 0
}
type DashboardAccess interface {
resource.StorageBackend
resource.ResourceIndexServer

View File

@ -14,8 +14,25 @@ var MySQL = mysql{
var _ Dialect = MySQL
type mysql struct {
standardIdent
backtickIdent
rowLockingClauseMap
argPlaceholderFunc
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
}
// 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 {
re *regexp.Regexp
replacement string

View File

@ -1,16 +1,26 @@
package dashboards
import (
"context"
"strings"
"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"
"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) {
testsuite.Run(m)
}
@ -45,6 +55,59 @@ func TestIntegrationDashboardsApp(t *testing.T) {
_, err := helper.NewDiscoveryClient().ServerResourcesForGroupVersion("dashboard.grafana.app/v0alpha1")
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) {
disco := helper.GetGroupVersionInfoJSON("dashboard.grafana.app")
// 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),
Login: fmt.Sprintf("%s-%d", name, orgId),
OrgID: orgId,
IsAdmin: basicRole == identity.RoleAdmin && orgId == 1, // make org1 admins grafana admins
})
require.NoError(c.t, err)
require.Equal(c.t, orgId, u.OrgID)