From b640b6e8d10d0cfed6d04051ebeed0a8df8e6219 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 5 Jun 2024 11:21:51 +0300 Subject: [PATCH] K8s: Improve k8s dashboard query (#88629) --- .../apis/dashboard/access/sql_dashboards.go | 95 +++++++++++++------ pkg/registry/apis/dashboard/access/token.go | 6 +- pkg/registry/apis/dashboard/access/types.go | 17 +++- pkg/registry/apis/dashboard/legacy_storage.go | 10 ++ .../apis/dashboard/summary_storage.go | 9 ++ 5 files changed, 100 insertions(+), 37 deletions(-) diff --git a/pkg/registry/apis/dashboard/access/sql_dashboards.go b/pkg/registry/apis/dashboard/access/sql_dashboards.go index a7dc2698706..30462de1a62 100644 --- a/pkg/registry/apis/dashboard/access/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/access/sql_dashboards.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "path/filepath" + "strings" "time" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -84,24 +85,71 @@ const selector = `SELECT LEFT OUTER JOIN user AS UpdatedUSER ON dashboard.created_by = UpdatedUSER.id WHERE is_folder = false` -// GetDashboards implements DashboardAccess. -func (a *dashboardSqlAccess) GetDashboards(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardList, error) { +func (a *dashboardSqlAccess) getRows(ctx context.Context, query *DashboardQuery, onlySummary bool) (*rowsWrapper, int, error) { + if !query.Labels.Empty() { + return nil, 0, fmt.Errorf("label selection not yet supported") + } + if len(query.Requirements.SortBy) > 0 { + return nil, 0, fmt.Errorf("sorting not yet supported") + } + if query.Requirements.ListHistory != "" { + return nil, 0, fmt.Errorf("ListHistory not yet supported") + } + if query.Requirements.ListDeleted { + return nil, 0, fmt.Errorf("ListDeleted not yet supported") + } + if len(query.Requirements.ListOriginKeys) > 0 { + return nil, 0, fmt.Errorf("ListOriginKeys not yet supported") + } + token, err := readContinueToken(query) if err != nil { - return nil, err + return nil, 0, err } limit := query.Limit if limit < 1 { limit = 15 // } + args := []any{query.OrgID} - rows, err := a.doQuery(ctx, selector+` - AND dashboard.org_id=$1 - AND dashboard.id>=$2 - ORDER BY dashboard.id asc - LIMIT $3 - `, query.OrgID, token.id, (limit + 2)) + sqlcmd := selector + + // We can not do this yet because title + tags are in the body + if onlySummary && false { + sqlcmd = strings.Replace(sqlcmd, "dashboard.data", `"{}"`, 1) + } + + sqlcmd = fmt.Sprintf("%s AND dashboard.org_id=$%d", sqlcmd, len(args)) + if query.UID != "" { + args = append(args, query.UID) + sqlcmd = fmt.Sprintf("%s AND dashboard.uid=$%d", sqlcmd, len(args)) + } else { + args = append(args, token.id) + sqlcmd = fmt.Sprintf("%s AND dashboard.id>=$%d", sqlcmd, len(args)) + } + + if query.Requirements.Folder != nil { + args = append(args, *query.Requirements.Folder) + sqlcmd = fmt.Sprintf("%s AND dashboard.folder_uid=$%d", sqlcmd, len(args)) + } + + args = append(args, (limit + 2)) // add more so we can include a next token + sqlcmd = fmt.Sprintf("%s ORDER BY dashboard.id asc LIMIT $%d", sqlcmd, len(args)) + + rows, err := a.doQuery(ctx, sqlcmd, args...) + if err != nil { + if rows != nil { + _ = rows.Close() + } + rows = nil + } + return rows, limit, err +} + +// GetDashboards implements DashboardAccess. +func (a *dashboardSqlAccess) GetDashboards(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardList, error) { + rows, limit, err := a.getRows(ctx, query, false) if err != nil { return nil, err } @@ -109,9 +157,6 @@ func (a *dashboardSqlAccess) GetDashboards(ctx context.Context, query *Dashboard totalSize := 0 list := &dashboardsV0.DashboardList{} - if err != nil { - return nil, err - } for { row, err := rows.Next() if err != nil || row == nil { @@ -120,7 +165,9 @@ func (a *dashboardSqlAccess) GetDashboards(ctx context.Context, query *Dashboard totalSize += row.Bytes if len(list.Items) > 0 && (totalSize > query.MaxBytes || len(list.Items) >= limit) { - row.token.folder = query.FolderUID + if query.Requirements.Folder != nil { + row.token.folder = *query.Requirements.Folder + } list.Continue = row.token.String() // will skip this one but start here next time return list, err } @@ -144,20 +191,7 @@ func (a *dashboardSqlAccess) GetDashboard(ctx context.Context, orgId int64, uid // GetDashboards implements DashboardAccess. func (a *dashboardSqlAccess) GetDashboardSummaries(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardSummaryList, error) { - token, err := readContinueToken(query) - if err != nil { - return nil, err - } - limit := query.Limit - if limit < 1 { - limit = 15 // - } - rows, err := a.doQuery(ctx, selector+` - AND dashboard.org_id=$1 - AND dashboard.id>=$2 - ORDER BY dashboard.id asc - LIMIT $3 - `, query.OrgID, token.id, (limit + 2)) + rows, limit, err := a.getRows(ctx, query, true) if err != nil { return nil, err } @@ -165,9 +199,6 @@ func (a *dashboardSqlAccess) GetDashboardSummaries(ctx context.Context, query *D totalSize := 0 list := &dashboardsV0.DashboardSummaryList{} - if err != nil { - return nil, err - } for { row, err := rows.Next() if err != nil || row == nil { @@ -176,7 +207,9 @@ func (a *dashboardSqlAccess) GetDashboardSummaries(ctx context.Context, query *D totalSize += row.Bytes if len(list.Items) > 0 && (totalSize > query.MaxBytes || len(list.Items) >= limit) { - row.token.folder = query.FolderUID + if query.Requirements.Folder != nil { + row.token.folder = *query.Requirements.Folder + } list.Continue = row.token.String() // will skip this one but start here next time return list, err } diff --git a/pkg/registry/apis/dashboard/access/token.go b/pkg/registry/apis/dashboard/access/token.go index 0805adb8913..f8b32bbcb5d 100644 --- a/pkg/registry/apis/dashboard/access/token.go +++ b/pkg/registry/apis/dashboard/access/token.go @@ -50,7 +50,11 @@ func readContinueToken(q *DashboardQuery) (continueToken, error) { token.folder = sub[1] // Check if the folder filter is the same from the previous query - if token.folder != q.FolderUID { + if q.Requirements.Folder == nil { + if token.folder != "" { + return token, fmt.Errorf("invalid token, the folder must match previous query") + } + } else if token.folder != *q.Requirements.Folder { return token, fmt.Errorf("invalid token, the folder must match previous query") } diff --git a/pkg/registry/apis/dashboard/access/types.go b/pkg/registry/apis/dashboard/access/types.go index 1df421caf6d..cf5df48e19f 100644 --- a/pkg/registry/apis/dashboard/access/types.go +++ b/pkg/registry/apis/dashboard/access/types.go @@ -3,17 +3,24 @@ package access import ( "context" + "k8s.io/apimachinery/pkg/labels" + dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver/storage/entity" ) // This does not check if you have permissions! type DashboardQuery struct { - OrgID int64 - UID string // to select a single dashboard - FolderUID string - Limit int - MaxBytes int + OrgID int64 + UID string // to select a single dashboard + Limit int + MaxBytes int + + // FolderUID etc + Requirements entity.Requirements + // Post processing label filter + Labels labels.Selector // The token from previous query ContinueToken string diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go index 7e2a9881678..f08ac34550f 100644 --- a/pkg/registry/apis/dashboard/legacy_storage.go +++ b/pkg/registry/apis/dashboard/legacy_storage.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/dashboard/access" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/apiserver/storage/entity" ) var ( @@ -136,12 +137,21 @@ func (s *dashboardStorage) List(ctx context.Context, options *internalversion.Li // fmt.Printf("LIST: %s\n", options.Continue) + // translate grafana.app/* label selectors into field requirements + requirements, newSelector, err := entity.ReadLabelSelectors(options.LabelSelector) + if err != nil { + return nil, err + } + query := &access.DashboardQuery{ OrgID: orgId, Limit: int(options.Limit), MaxBytes: 2 * 1024 * 1024, // 2MB, ContinueToken: options.Continue, + Requirements: requirements, + Labels: newSelector, } + return s.access.GetDashboards(ctx, query) } diff --git a/pkg/registry/apis/dashboard/summary_storage.go b/pkg/registry/apis/dashboard/summary_storage.go index 9f8966e012e..85fe3bff0ff 100644 --- a/pkg/registry/apis/dashboard/summary_storage.go +++ b/pkg/registry/apis/dashboard/summary_storage.go @@ -11,6 +11,7 @@ import ( common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/dashboard/access" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/apiserver/storage/entity" ) var ( @@ -55,11 +56,19 @@ func (s *summaryStorage) List(ctx context.Context, options *internalversion.List return nil, err } + // translate grafana.app/* label selectors into field requirements + requirements, newSelector, err := entity.ReadLabelSelectors(options.LabelSelector) + if err != nil { + return nil, err + } + query := &access.DashboardQuery{ OrgID: orgId, Limit: int(options.Limit), MaxBytes: 2 * 1024 * 1024, // 2MB, ContinueToken: options.Continue, + Requirements: requirements, + Labels: newSelector, } return s.access.GetDashboardSummaries(ctx, query) }