From 9e116d13a599824ddf665c7b95220f3d0c034db8 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 7 Aug 2024 13:43:13 +0300 Subject: [PATCH] K8s/Dashboards: Fix dashboard list and add tests (#91523) --- pkg/registry/apis/dashboard/legacy/queries.go | 38 ++++ .../apis/dashboard/legacy/queries_test.go | 167 ++++++++++++++++ .../dashboard/legacy/query_dashboards.sql | 45 +++++ .../apis/dashboard/legacy/sql_dashboards.go | 184 +++++++----------- pkg/registry/apis/dashboard/legacy/storage.go | 39 ++-- .../legacy/testdata/mysql__dashboard.sql | 18 ++ .../testdata/mysql__dashboard_next_page.sql | 20 ++ .../legacy/testdata/mysql__history_uid.sql | 19 ++ .../mysql__history_uid_at_version.sql | 20 ++ .../mysql__history_uid_second_page.sql | 20 ++ .../legacy/testdata/postgres__dashboard.sql | 18 ++ .../postgres__dashboard_next_page.sql | 20 ++ .../legacy/testdata/postgres__history_uid.sql | 19 ++ .../postgres__history_uid_at_version.sql | 20 ++ .../postgres__history_uid_second_page.sql | 20 ++ .../legacy/testdata/sqlite__dashboard.sql | 18 ++ .../testdata/sqlite__dashboard_next_page.sql | 20 ++ .../legacy/testdata/sqlite__history_uid.sql | 19 ++ .../sqlite__history_uid_at_version.sql | 20 ++ .../sqlite__history_uid_second_page.sql | 20 ++ pkg/registry/apis/dashboard/legacy/types.go | 11 +- .../unified/sql/sqltemplate/dialect_mysql.go | 19 +- .../unified/sql/sqltemplate/sqltemplate.go | 16 ++ pkg/tests/apis/dashboard/dashboards_test.go | 67 ++++++- pkg/tests/apis/helper.go | 1 + 25 files changed, 740 insertions(+), 138 deletions(-) create mode 100644 pkg/registry/apis/dashboard/legacy/queries.go create mode 100644 pkg/registry/apis/dashboard/legacy/queries_test.go create mode 100644 pkg/registry/apis/dashboard/legacy/query_dashboards.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/mysql__dashboard.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/mysql__dashboard_next_page.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid_at_version.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid_second_page.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/postgres__dashboard.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/postgres__dashboard_next_page.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid_at_version.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid_second_page.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/sqlite__dashboard.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/sqlite__dashboard_next_page.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/sqlite__history_uid.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/sqlite__history_uid_at_version.sql create mode 100755 pkg/registry/apis/dashboard/legacy/testdata/sqlite__history_uid_second_page.sql diff --git a/pkg/registry/apis/dashboard/legacy/queries.go b/pkg/registry/apis/dashboard/legacy/queries.go new file mode 100644 index 00000000000..cc92980b310 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/queries.go @@ -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 +} diff --git a/pkg/registry/apis/dashboard/legacy/queries_test.go b/pkg/registry/apis/dashboard/legacy/queries_test.go new file mode 100644 index 00000000000..4d596bd2090 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/queries_test.go @@ -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, "") + 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) + } + }) + } + }) + } + }) + } +} diff --git a/pkg/registry/apis/dashboard/legacy/query_dashboards.sql b/pkg/registry/apis/dashboard/legacy/query_dashboards.sql new file mode 100644 index 00000000000..1d09adef307 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/query_dashboards.sql @@ -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 }} diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 8f08769032c..23938a00b23 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -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. diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index 14c32b05eee..837b86b6fb1 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -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 // } diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql__dashboard.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql__dashboard.sql new file mode 100755 index 00000000000..20109d629c2 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql__dashboard.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql__dashboard_next_page.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql__dashboard_next_page.sql new file mode 100755 index 00000000000..b6ca893dd04 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql__dashboard_next_page.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid.sql new file mode 100755 index 00000000000..f78abfda947 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid_at_version.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid_at_version.sql new file mode 100755 index 00000000000..628913ed3fc --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid_at_version.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid_second_page.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid_second_page.sql new file mode 100755 index 00000000000..240e937dead --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid_second_page.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres__dashboard.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres__dashboard.sql new file mode 100755 index 00000000000..c92b21bb165 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres__dashboard.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres__dashboard_next_page.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres__dashboard_next_page.sql new file mode 100755 index 00000000000..437a4c4bfbb --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres__dashboard_next_page.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid.sql new file mode 100755 index 00000000000..db6f9b3a29a --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid_at_version.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid_at_version.sql new file mode 100755 index 00000000000..ed5baa52705 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid_at_version.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid_second_page.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid_second_page.sql new file mode 100755 index 00000000000..692a44fcf67 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid_second_page.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite__dashboard.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite__dashboard.sql new file mode 100755 index 00000000000..764fbb0d5e2 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite__dashboard.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite__dashboard_next_page.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite__dashboard_next_page.sql new file mode 100755 index 00000000000..9ad7aeecb70 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite__dashboard_next_page.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite__history_uid.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite__history_uid.sql new file mode 100755 index 00000000000..34135c79e17 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite__history_uid.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite__history_uid_at_version.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite__history_uid_at_version.sql new file mode 100755 index 00000000000..5bf79fbcec0 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite__history_uid_at_version.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite__history_uid_second_page.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite__history_uid_second_page.sql new file mode 100755 index 00000000000..5d28ea3f625 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite__history_uid_second_page.sql @@ -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 diff --git a/pkg/registry/apis/dashboard/legacy/types.go b/pkg/registry/apis/dashboard/legacy/types.go index 690a9854739..ba3a0e61251 100644 --- a/pkg/registry/apis/dashboard/legacy/types.go +++ b/pkg/registry/apis/dashboard/legacy/types.go @@ -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 diff --git a/pkg/storage/unified/sql/sqltemplate/dialect_mysql.go b/pkg/storage/unified/sql/sqltemplate/dialect_mysql.go index 705c8c9eca4..5fd5ca42c44 100644 --- a/pkg/storage/unified/sql/sqltemplate/dialect_mysql.go +++ b/pkg/storage/unified/sql/sqltemplate/dialect_mysql.go @@ -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) +} diff --git a/pkg/storage/unified/sql/sqltemplate/sqltemplate.go b/pkg/storage/unified/sql/sqltemplate/sqltemplate.go index 551a23fca62..c88a443deec 100644 --- a/pkg/storage/unified/sql/sqltemplate/sqltemplate.go +++ b/pkg/storage/unified/sql/sqltemplate/sqltemplate.go @@ -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 diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index 6ff14ab4fff..1fb328153b6 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -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)) diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index 3bee282dafd..4d896fa5690 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -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)