diff --git a/pkg/apimachinery/identity/namespace.go b/pkg/apimachinery/identity/namespace.go index aa3a38d3994..2b72ae71f58 100644 --- a/pkg/apimachinery/identity/namespace.go +++ b/pkg/apimachinery/identity/namespace.go @@ -15,6 +15,7 @@ const ( NamespaceAnonymous Namespace = "anonymous" NamespaceRenderService Namespace = "render" NamespaceAccessPolicy Namespace = "access-policy" + NamespaceProvisioning Namespace = "provisioning" NamespaceEmpty Namespace = "" ) diff --git a/pkg/registry/apis/dashboard/access/types.go b/pkg/registry/apis/dashboard/access/types.go deleted file mode 100644 index 50065c8f2a2..00000000000 --- a/pkg/registry/apis/dashboard/access/types.go +++ /dev/null @@ -1,35 +0,0 @@ -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 - Limit int - MaxBytes int - - // FolderUID etc - Requirements entity.Requirements - // Post processing label filter - Labels labels.Selector - - // The token from previous query - ContinueToken string -} - -type DashboardAccess interface { - GetDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, error) - GetDashboards(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardList, error) - - SaveDashboard(ctx context.Context, orgId int64, dash *dashboardsV0.Dashboard) (*dashboardsV0.Dashboard, bool, error) - DeleteDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, bool, error) -} diff --git a/pkg/registry/apis/dashboard/legacy/README.md b/pkg/registry/apis/dashboard/legacy/README.md new file mode 100644 index 00000000000..660bb821efd --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/README.md @@ -0,0 +1,12 @@ +This implements a ResourceServer backed by the existing dashboard SQL tables. + +There are a few oddities worth noting. This is not a totally accurate implementation, +but it is good enough to drive the UI needs and let kubectl list work! + +1. The resourceVersion is based on internal ID and dashboard version + - can get version from the least significant digits + - avoids duplicate resourceVersions... but not sequential + - the resourceVersion is never set on the list commands + +1. Results are always sorted by internal id ascending + - this ensures everything is returned diff --git a/pkg/registry/apis/dashboard/access/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go similarity index 59% rename from pkg/registry/apis/dashboard/access/sql_dashboards.go rename to pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 86bbbfe1239..9347f389609 100644 --- a/pkg/registry/apis/dashboard/access/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -1,27 +1,28 @@ -package access +package legacy import ( "context" "database/sql" "fmt" "path/filepath" - "strings" + "sync" "time" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" + 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" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" 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/session" + "github.com/grafana/grafana/pkg/storage/unified/resource" ) var ( @@ -29,18 +30,15 @@ var ( ) type dashboardRow struct { + // The numeric version for this dashboard + RV int64 + // Dashboard resource Dash *dashboardsV0.Dashboard - // Title -- this may come from saved metadata rather than the body - Title string - // The folder UID (needed for access control checks) FolderUID string - // Needed for fast summary access - Tags []string - // Size (in bytes) of the dashboard payload Bytes int @@ -55,9 +53,17 @@ type dashboardSqlAccess struct { namespacer request.NamespaceMapper dashStore dashboards.Store provisioning provisioning.ProvisioningService + + // Typically one... the server wrapper + subscribers []chan *resource.WrittenEvent + mutex sync.Mutex } -func NewDashboardAccess(sql db.DB, namespacer request.NamespaceMapper, dashStore dashboards.Store, provisioning provisioning.ProvisioningService) DashboardAccess { +func NewDashboardAccess(sql db.DB, + namespacer request.NamespaceMapper, + dashStore dashboards.Store, + provisioning provisioning.ProvisioningService, +) DashboardAccess { return &dashboardSqlAccess{ sql: sql, sess: sql.GetSqlxSession(), @@ -69,72 +75,100 @@ func NewDashboardAccess(sql db.DB, namespacer request.NamespaceMapper, dashStore const selector = `SELECT dashboard.org_id, dashboard.id, - dashboard.uid,slug, - dashboard.folder_uid, - dashboard.created,dashboard.created_by,CreatedUSER.login, - dashboard.updated,dashboard.updated_by,UpdatedUSER.login, - plugin_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, - title, - dashboard.data + 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.created_by = UpdatedUSER.id - WHERE is_folder = false` + LEFT OUTER JOIN user AS UpdatedUSER ON dashboard.updated_by = UpdatedUSER.id + WHERE dashboard.is_folder = false` -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") +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, int, error) { + if len(query.Labels) > 0 { + return nil, 0, 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)) + // } } - token, err := readContinueToken(query) - if err != nil { - return nil, 0, err - } + var sqlcmd string + args := []any{query.OrgID} limit := query.Limit if limit < 1 { limit = 15 // } - args := []any{query.OrgID} - sqlcmd := selector + if query.GetHistory || query.Version > 0 { + if query.GetTrash { + return nil, 0, fmt.Errorf("trash not included in history table") + } - // 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\n ", history, len(args)) + + if query.UID == "" { + return nil, 0, fmt.Errorf("history query must have a UID") + } - 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)) + + 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)) + } + + args = append(args, (limit + 2)) // add more so we can include a next token + sqlcmd = fmt.Sprintf("%s\n ORDER BY dashboard_version.version desc LIMIT $%d", sqlcmd, len(args)) } else { - args = append(args, token.id) - sqlcmd = fmt.Sprintf("%s AND dashboard.id>=$%d", sqlcmd, len(args)) - } + sqlcmd = fmt.Sprintf("%s AND dashboard.org_id=$%d\n ", selector, 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)) - } + 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" + } - 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)) + args = append(args, (limit + 2)) // add more so we can include a next token + sqlcmd = fmt.Sprintf("%s\n ORDER BY dashboard.id asc LIMIT $%d", sqlcmd, len(args)) + } + // fmt.Printf("%s // %v\n", sqlcmd, args) rows, err := a.doQuery(ctx, sqlcmd, args...) if err != nil { @@ -146,51 +180,8 @@ func (a *dashboardSqlAccess) getRows(ctx context.Context, query *DashboardQuery, 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 - } - defer func() { _ = rows.Close() }() - - totalSize := 0 - list := &dashboardsV0.DashboardList{} - for { - row, err := rows.Next() - if err != nil || row == nil { - return list, err - } - - totalSize += row.Bytes - if len(list.Items) > 0 && (totalSize > query.MaxBytes || len(list.Items) >= limit) { - 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 - } - list.Items = append(list.Items, *row.Dash) - } -} - -func (a *dashboardSqlAccess) GetDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, error) { - r, err := a.GetDashboards(ctx, &DashboardQuery{ - OrgID: orgId, - UID: uid, - Labels: labels.Everything(), - }) - if err != nil { - return nil, err - } - if len(r.Items) > 0 { - return &r.Items[0], nil - } - return nil, fmt.Errorf("not found") -} - func (a *dashboardSqlAccess) doQuery(ctx context.Context, query string, args ...any) (*rowsWrapper, error) { - user, err := appcontext.User(ctx) + _, err := identity.GetRequester(ctx) if err != nil { return nil, err } @@ -199,7 +190,10 @@ func (a *dashboardSqlAccess) doQuery(ctx context.Context, query string, args ... rows: rows, a: a, // This looks up rules from the permissions on a user - canReadDashboard: accesscontrol.Checker(user, dashboards.ActionDashboardsRead), + canReadDashboard: func(scopes ...string) bool { + return true // ??? + }, + // accesscontrol.Checker(user, dashboards.ActionDashboardsRead), }, err } @@ -230,7 +224,6 @@ func (r *rowsWrapper) Next() (*dashboardRow, error) { if !r.canReadDashboard(scopes...) { continue } - d.token.size = r.total // size before next! r.total += int64(d.Bytes) } @@ -243,21 +236,20 @@ func (r *rowsWrapper) Next() (*dashboardRow, error) { func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { dash := &dashboardsV0.Dashboard{ TypeMeta: dashboardsV0.DashboardResourceInfo.TypeMeta(), - ObjectMeta: v1.ObjectMeta{Annotations: make(map[string]string)}, + ObjectMeta: metav1.ObjectMeta{Annotations: make(map[string]string)}, } row := &dashboardRow{Dash: dash} var dashboard_id int64 var orgId int64 - var slug string var folder_uid sql.NullString var updated time.Time - var updatedByID int64 - var updatedByName sql.NullString + var updatedBy sql.NullString + var deleted sql.NullTime var created time.Time - var createdByID int64 - var createdByName sql.NullString + var createdBy sql.NullString + var message sql.NullString var plugin_id string var origin_name sql.NullString @@ -267,40 +259,42 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { var data []byte // the dashboard JSON var version int64 - err := rows.Scan(&orgId, &dashboard_id, &dash.Name, - &slug, &folder_uid, - &created, &createdByID, &createdByName, - &updated, &updatedByID, &updatedByName, - &plugin_id, + 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, - &version, - &row.Title, &data, + &version, &message, &data, ) row.token = &continueToken{orgId: orgId, id: dashboard_id} if err == nil { - dash.ResourceVersion = fmt.Sprintf("%d", created.UnixMilli()) + row.RV = getResourceVersion(dashboard_id, version) + dash.ResourceVersion = fmt.Sprintf("%d", row.RV) dash.Namespace = a.namespacer(orgId) dash.UID = gapiutil.CalculateClusterWideUID(dash) - dash.SetCreationTimestamp(v1.NewTime(created)) + dash.SetCreationTimestamp(metav1.NewTime(created)) meta, err := utils.MetaAccessor(dash) if err != nil { return nil, err } meta.SetUpdatedTimestamp(&updated) - meta.SetSlug(slug) - if createdByID > 0 { - meta.SetCreatedBy(fmt.Sprintf("user:%d/%s", createdByID, createdByName.String)) + meta.SetCreatedBy(getUserID(createdBy)) + meta.SetUpdatedBy(getUserID(updatedBy)) + + if deleted.Valid { + meta.SetDeletionTimestamp(ptr.To(metav1.NewTime(deleted.Time))) } - if updatedByID > 0 { - meta.SetUpdatedBy(fmt.Sprintf("user:%d/%s", updatedByID, updatedByName.String)) + + if message.String != "" { + meta.SetMessage(message.String) } - if folder_uid.Valid { + if folder_uid.String != "" { meta.SetFolder(folder_uid.String) row.FolderUID = folder_uid.String } - if origin_name.Valid { + if origin_name.String != "" { ts := time.Unix(origin_ts.Int64, 0) resolvedPath := a.provisioning.GetDashboardProvisionerResolvedPath(origin_name.String) @@ -332,16 +326,21 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { return row, err } dash.Spec.Set("id", dashboard_id) // add it so we can get it from the body later - row.Title = dash.Spec.GetNestedString("title") - row.Tags = dash.Spec.GetNestedStringSlice("tags") } } return row, err } +func getUserID(v sql.NullString) string { + if v.String == "" { + return identity.NewNamespaceIDString(identity.NamespaceProvisioning, "").String() + } + return identity.NewNamespaceIDString(identity.NamespaceUser, v.String).String() +} + // DeleteDashboard implements DashboardAccess. func (a *dashboardSqlAccess) DeleteDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, bool, error) { - dash, err := a.GetDashboard(ctx, orgId, uid) + dash, _, err := a.GetDashboard(ctx, orgId, uid, 0) if err != nil { return nil, false, err } @@ -404,6 +403,6 @@ func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, das if out != nil { created = (out.Created.Unix() == out.Updated.Unix()) // and now? } - dash, err = a.GetDashboard(ctx, orgId, out.UID) + dash, _, err = a.GetDashboard(ctx, orgId, out.UID, 0) return dash, created, err } diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go new file mode 100644 index 00000000000..21711f30b96 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -0,0 +1,329 @@ +package legacy + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +func getDashboardFromEvent(event resource.WriteEvent) (*dashboard.Dashboard, error) { + obj, ok := event.Object.GetRuntimeObject() + if ok && obj != nil { + dash, ok := obj.(*dashboard.Dashboard) + if ok { + return dash, nil + } + } + dash := &dashboard.Dashboard{} + err := json.Unmarshal(event.Value, dash) + return dash, err +} + +func isDashboardKey(key *resource.ResourceKey, requireName bool) error { + gr := dashboard.DashboardResourceInfo.GroupResource() + if key.Group != gr.Group { + return fmt.Errorf("expecting dashboard group (%s != %s)", key.Group, gr.Group) + } + if key.Resource != gr.Resource { + return fmt.Errorf("expecting dashboard resource (%s != %s)", key.Resource, gr.Resource) + } + if requireName && key.Name == "" { + return fmt.Errorf("expecting dashboard name (uid)") + } + return nil +} + +func (a *dashboardSqlAccess) WriteEvent(ctx context.Context, event resource.WriteEvent) (rv int64, err error) { + info, err := request.ParseNamespace(event.Key.Namespace) + if err == nil { + err = isDashboardKey(event.Key, true) + } + if err != nil { + return 0, err + } + + switch event.Type { + case resource.WatchEvent_DELETED: + { + _, _, err = a.DeleteDashboard(ctx, info.OrgID, event.Key.Name) + //rv = ??? + } + // The difference depends on embedded internal ID + case resource.WatchEvent_ADDED, resource.WatchEvent_MODIFIED: + { + dash, err := getDashboardFromEvent(event) + if err != nil { + return 0, err + } + + after, _, err := a.SaveDashboard(ctx, info.OrgID, dash) + if err != nil { + return 0, err + } + if after != nil { + meta, err := utils.MetaAccessor(after) + if err != nil { + return 0, err + } + rv, err = meta.GetResourceVersionInt64() + if err != nil { + return 0, err + } + } + } + default: + return 0, fmt.Errorf("unsupported event type: %v", event.Type) + } + + // Async notify all subscribers (not HA!!!) + if a.subscribers != nil { + go func() { + write := &resource.WrittenEvent{ + WriteEvent: event, + + Timestamp: time.Now().UnixMilli(), + ResourceVersion: rv, + } + for _, sub := range a.subscribers { + sub <- write + } + }() + } + return rv, err +} + +// Read implements ResourceStoreServer. +func (a *dashboardSqlAccess) GetDashboard(ctx context.Context, orgId int64, uid string, v int64) (*dashboard.Dashboard, int64, error) { + rows, _, err := a.getRows(ctx, &DashboardQuery{ + OrgID: orgId, + UID: uid, + Limit: 2, // will only be one! + Version: v, + }) + if err != nil { + return nil, 0, err + } + defer func() { _ = rows.Close() }() + + row, err := rows.Next() + if err != nil || row == nil { + return nil, 0, err + } + return row.Dash, row.RV, nil +} + +// Read implements ResourceStoreServer. +func (a *dashboardSqlAccess) Read(ctx context.Context, req *resource.ReadRequest) (*resource.ReadResponse, error) { + info, err := request.ParseNamespace(req.Key.Namespace) + if err == nil { + err = isDashboardKey(req.Key, true) + } + if err != nil { + return nil, err + } + version := int64(0) + if req.ResourceVersion > 0 { + version = getVersionFromRV(req.ResourceVersion) + } + + dash, rv, err := a.GetDashboard(ctx, info.OrgID, req.Key.Name, version) + if err != nil { + return nil, err + } + if dash == nil { + return &resource.ReadResponse{ + Error: &resource.ErrorResult{ + Code: http.StatusNotFound, + }, + }, err + } + + value, err := json.Marshal(dash) + return &resource.ReadResponse{ + ResourceVersion: rv, + Value: value, + }, err +} + +// List implements AppendingStore. +func (a *dashboardSqlAccess) PrepareList(ctx context.Context, req *resource.ListRequest) (*resource.ListResponse, error) { + opts := req.Options + info, err := request.ParseNamespace(opts.Key.Namespace) + if err == nil { + err = isDashboardKey(opts.Key, false) + } + if err != nil { + return nil, err + } + + token, err := readContinueToken(req.NextPageToken) + if err != nil { + return nil, err + } + if token.orgId > 0 && token.orgId != info.OrgID { + return nil, fmt.Errorf("token and orgID mismatch") + } + + query := &DashboardQuery{ + OrgID: info.OrgID, + Limit: int(req.Limit), + MaxBytes: 2 * 1024 * 1024, // 2MB, + LastID: token.id, + Labels: req.Options.Labels, + } + + rows, limit, err := a.getRows(ctx, query) + if err != nil { + return nil, err + } + defer func() { _ = rows.Close() }() + + totalSize := 0 + list := &resource.ListResponse{} + for { + row, err := rows.Next() + if err != nil || row == nil { + return list, err + } + + totalSize += row.Bytes + if len(list.Items) > 0 && (totalSize > query.MaxBytes || len(list.Items) >= limit) { + // if query.Requirements.Folder != nil { + // row.token.folder = *query.Requirements.Folder + // } + list.NextPageToken = row.token.String() // will skip this one but start here next time + return list, err + } + // TODO -- make it smaller and stick the body as an annotation... + val, err := json.Marshal(row.Dash) + if err != nil { + return list, err + } + list.Items = append(list.Items, &resource.ResourceWrapper{ + ResourceVersion: row.RV, + Value: val, + }) + } +} + +// Watch implements AppendingStore. +func (a *dashboardSqlAccess) WatchWriteEvents(ctx context.Context) (<-chan *resource.WrittenEvent, error) { + stream := make(chan *resource.WrittenEvent, 10) + { + a.mutex.Lock() + defer a.mutex.Unlock() + + // Add the event stream + a.subscribers = append(a.subscribers, stream) + } + + // Wait for context done + go func() { + // Wait till the context is done + <-ctx.Done() + + // Then remove the subscription + a.mutex.Lock() + defer a.mutex.Unlock() + + // Copy all streams without our listener + subs := []chan *resource.WrittenEvent{} + for _, sub := range a.subscribers { + if sub != stream { + subs = append(subs, sub) + } + } + a.subscribers = subs + }() + return stream, nil +} + +func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryRequest) (*resource.HistoryResponse, error) { + info, err := request.ParseNamespace(req.Key.Namespace) + if err == nil { + err = isDashboardKey(req.Key, false) + } + if err != nil { + return nil, err + } + + token, err := readContinueToken(req.NextPageToken) + if err != nil { + return nil, err + } + if token.orgId > 0 && token.orgId != info.OrgID { + return nil, fmt.Errorf("token and orgID mismatch") + } + + query := &DashboardQuery{ + OrgID: info.OrgID, + Limit: int(req.Limit), + MaxBytes: 2 * 1024 * 1024, // 2MB, + LastID: token.id, + UID: req.Key.Name, + } + if req.ShowDeleted { + query.GetTrash = true + } else { + query.GetHistory = true + } + + rows, limit, err := a.getRows(ctx, query) + if err != nil { + return nil, err + } + defer func() { _ = rows.Close() }() + + totalSize := 0 + list := &resource.HistoryResponse{} + for { + row, err := rows.Next() + if err != nil || row == nil { + return list, err + } + + totalSize += row.Bytes + if len(list.Items) > 0 && (totalSize > query.MaxBytes || len(list.Items) >= limit) { + // if query.Requirements.Folder != nil { + // row.token.folder = *query.Requirements.Folder + // } + row.token.id = getVersionFromRV(row.RV) // Use the version as the increment + list.NextPageToken = row.token.String() // will skip this one but start here next time + return list, err + } + + partial := &metav1.PartialObjectMetadata{ + ObjectMeta: row.Dash.ObjectMeta, + } + partial.UID = "" // it is not useful/helpful/accurate and just confusing now + + val, err := json.Marshal(partial) + if err != nil { + return list, err + } + full, err := json.Marshal(row.Dash.Spec) + if err != nil { + return list, err + } + list.Items = append(list.Items, &resource.ResourceMeta{ + ResourceVersion: row.RV, + PartialObjectMeta: val, + Size: int32(len(full)), + Hash: "??", // hash the full? + }) + } +} + +// Used for efficient provisioning +func (a *dashboardSqlAccess) Origin(context.Context, *resource.OriginRequest) (*resource.OriginResponse, error) { + return nil, fmt.Errorf("not yet (origin)") +} diff --git a/pkg/registry/apis/dashboard/access/token.go b/pkg/registry/apis/dashboard/legacy/token.go similarity index 60% rename from pkg/registry/apis/dashboard/access/token.go rename to pkg/registry/apis/dashboard/legacy/token.go index f8b32bbcb5d..a3bc0e5fb90 100644 --- a/pkg/registry/apis/dashboard/access/token.go +++ b/pkg/registry/apis/dashboard/legacy/token.go @@ -1,27 +1,24 @@ -package access +package legacy import ( "fmt" "strconv" "strings" - - "github.com/grafana/grafana/pkg/util" ) type continueToken struct { orgId int64 id int64 // the internal id (sort by!) folder string // from the query - size int64 } -func readContinueToken(q *DashboardQuery) (continueToken, error) { +func readContinueToken(next string) (continueToken, error) { var err error token := continueToken{} - if q.ContinueToken == "" { + if next == "" { return token, nil } - parts := strings.Split(q.ContinueToken, "/") + parts := strings.Split(next, "/") if len(parts) < 3 { return token, fmt.Errorf("invalid continue token (too few parts)") } @@ -49,19 +46,19 @@ func readContinueToken(q *DashboardQuery) (continueToken, error) { } token.folder = sub[1] - // Check if the folder filter is the same from the previous query - 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") - } + // // Check if the folder filter is the same from the previous query + // 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") + // } return token, err } func (r *continueToken) String() string { - return fmt.Sprintf("org:%d/start:%d/folder:%s/%s", - r.orgId, r.id, r.folder, util.ByteCountSI(r.size)) + return fmt.Sprintf("org:%d/start:%d/folder:%s", + r.orgId, r.id, r.folder) } diff --git a/pkg/registry/apis/dashboard/legacy/types.go b/pkg/registry/apis/dashboard/legacy/types.go new file mode 100644 index 00000000000..690a9854739 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/types.go @@ -0,0 +1,40 @@ +package legacy + +import ( + "context" + + dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +// This does not check if you have permissions! + +type DashboardQuery struct { + OrgID int64 + UID string // to select a single dashboard + Limit int + MaxBytes int + + // Included in the continue token + // This is the ID from the last dashboard sent in the previous page + LastID int64 + + // List dashboards with a deletion timestamp + GetTrash bool + + // Get dashboards from the history table + GetHistory bool + Version int64 + + // The label requirements + Labels []*resource.Requirement +} + +type DashboardAccess interface { + resource.StorageBackend + resource.ResourceIndexServer + + GetDashboard(ctx context.Context, orgId int64, uid string, version int64) (*dashboardsV0.Dashboard, int64, error) + SaveDashboard(ctx context.Context, orgId int64, dash *dashboardsV0.Dashboard) (*dashboardsV0.Dashboard, bool, error) + DeleteDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, bool, error) +} diff --git a/pkg/registry/apis/dashboard/legacy/utils.go b/pkg/registry/apis/dashboard/legacy/utils.go new file mode 100644 index 00000000000..c61db27788a --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/utils.go @@ -0,0 +1,9 @@ +package legacy + +func getResourceVersion(id int64, version int64) int64 { + return version + (id * 10000000) +} + +func getVersionFromRV(rv int64) int64 { + return rv % 10000000 +} diff --git a/pkg/registry/apis/dashboard/legacy/utils_test.go b/pkg/registry/apis/dashboard/legacy/utils_test.go new file mode 100644 index 00000000000..a058dae612a --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/utils_test.go @@ -0,0 +1,13 @@ +package legacy + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVersionHacks(t *testing.T) { + rv := getResourceVersion(123, 456) + require.Equal(t, int64(1230000456), rv) + require.Equal(t, int64(456), getVersionFromRV(rv)) +} diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go index f08ac34550f..746d3fcc32d 100644 --- a/pkg/registry/apis/dashboard/legacy_storage.go +++ b/pkg/registry/apis/dashboard/legacy_storage.go @@ -1,170 +1,70 @@ package dashboard import ( - "context" - "fmt" - "strings" - "time" - - "k8s.io/apimachinery/pkg/apis/meta/internalversion" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/registry/rest" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - "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 ( - _ rest.Storage = (*dashboardStorage)(nil) - _ rest.Scoper = (*dashboardStorage)(nil) - _ rest.SingularNameProvider = (*dashboardStorage)(nil) - _ rest.Getter = (*dashboardStorage)(nil) - _ rest.Lister = (*dashboardStorage)(nil) - _ rest.Creater = (*dashboardStorage)(nil) - _ rest.Updater = (*dashboardStorage)(nil) - _ rest.GracefulDeleter = (*dashboardStorage)(nil) + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/storage/unified/apistore" + "github.com/grafana/grafana/pkg/storage/unified/resource" ) type dashboardStorage struct { resource common.ResourceInfo - access access.DashboardAccess + access legacy.DashboardAccess tableConverter rest.TableConvertor + + server resource.ResourceServer } -func (s *dashboardStorage) New() runtime.Object { - return s.resource.NewFunc() -} - -func (s *dashboardStorage) Destroy() {} - -func (s *dashboardStorage) NamespaceScoped() bool { - return true -} - -func (s *dashboardStorage) GetSingularName() string { - return s.resource.GetSingularName() -} - -func (s *dashboardStorage) NewList() runtime.Object { - return s.resource.NewListFunc() -} - -func (s *dashboardStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { - return s.tableConverter.ConvertToTable(ctx, object, tableOptions) -} - -func (s *dashboardStorage) Create(ctx context.Context, - obj runtime.Object, - createValidation rest.ValidateObjectFunc, - options *metav1.CreateOptions, -) (runtime.Object, error) { - info, err := request.NamespaceInfoFrom(ctx, true) +func (s *dashboardStorage) newStore(scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter) (grafanarest.LegacyStorage, error) { + server, err := resource.NewResourceServer(resource.ResourceServerOptions{ + Backend: s.access, + Index: s.access, + // WriteAccess: resource.WriteAccessHooks{ + // Folder: func(ctx context.Context, user identity.Requester, uid string) bool { + // // ??? + // }, + // }, + }) if err != nil { return nil, err } + s.server = server - p, ok := obj.(*v0alpha1.Dashboard) - if !ok { - return nil, fmt.Errorf("expected dashboard?") - } - - // HACK to simplify unique name testing from kubectl - t := p.Spec.GetNestedString("title") - if strings.Contains(t, "${NOW}") { - t = strings.ReplaceAll(t, "${NOW}", fmt.Sprintf("%d", time.Now().Unix())) - p.Spec.Set("title", t) - } - - dash, _, err := s.access.SaveDashboard(ctx, info.OrgID, p) - return dash, err -} - -func (s *dashboardStorage) Update(ctx context.Context, - name string, - objInfo rest.UpdatedObjectInfo, - createValidation rest.ValidateObjectFunc, - updateValidation rest.ValidateObjectUpdateFunc, - forceAllowCreate bool, - options *metav1.UpdateOptions, -) (runtime.Object, bool, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, false, err - } - - created := false - old, err := s.Get(ctx, name, nil) - if err != nil { - return old, created, err - } - - obj, err := objInfo.UpdatedObject(ctx, old) - if err != nil { - return old, created, err - } - p, ok := obj.(*v0alpha1.Dashboard) - if !ok { - return nil, created, fmt.Errorf("expected dashboard after update") - } - - _, created, err = s.access.SaveDashboard(ctx, info.OrgID, p) - if err == nil { - r, err := s.Get(ctx, name, nil) - return r, created, err - } - return nil, created, err -} - -// GracefulDeleter -func (s *dashboardStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, false, err - } - - return s.access.DeleteDashboard(ctx, info.OrgID, name) -} - -func (s *dashboardStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - orgId, err := request.OrgIDForList(ctx) + resourceInfo := s.resource + defaultOpts, err := defaultOptsGetter.GetRESTOptions(resourceInfo.GroupResource()) if err != nil { return nil, err } + client := resource.NewLocalResourceStoreClient(server) + optsGetter := apistore.NewRESTOptionsGetter(client, + defaultOpts.StorageConfig.Codec, + ) - // fmt.Printf("LIST: %s\n", options.Continue) + strategy := grafanaregistry.NewStrategy(scheme) + store := &genericregistry.Store{ + NewFunc: resourceInfo.NewFunc, + NewListFunc: resourceInfo.NewListFunc, + KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()), + KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()), + PredicateFunc: grafanaregistry.Matcher, + DefaultQualifiedResource: resourceInfo.GroupResource(), + SingularQualifiedResource: resourceInfo.SingularGroupResource(), + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + TableConvertor: s.tableConverter, + } - // translate grafana.app/* label selectors into field requirements - requirements, newSelector, err := entity.ReadLabelSelectors(options.LabelSelector) - if err != nil { + options := &generic.StoreOptions{RESTOptions: optsGetter} + if err := store.CompleteWithOptions(options); 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) -} - -func (s *dashboardStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - return s.access.GetDashboard(ctx, info.OrgID, name) -} - -// GracefulDeleter -func (s *dashboardStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { - return nil, fmt.Errorf("DeleteCollection for dashboards not implemented") + return store, err } diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index f10ad89fb29..2e583d8588a 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -1,6 +1,10 @@ package dashboard import ( + "fmt" + "time" + + "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -11,22 +15,22 @@ import ( common "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" - "github.com/prometheus/client_golang/prometheus" - - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/registry/apis/dashboard/access" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/dashboards" - dashver "github.com/grafana/grafana/pkg/services/dashboardversion" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/apistore" ) var _ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil) @@ -35,11 +39,8 @@ var _ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil) type DashboardsAPIBuilder struct { dashboardService dashboards.DashboardService - dashboardVersionService dashver.Service - accessControl accesscontrol.AccessControl - namespacer request.NamespaceMapper - access access.DashboardAccess - dashStore dashboards.Store + accessControl accesscontrol.AccessControl + legacy *dashboardStorage log log.Logger } @@ -47,12 +48,12 @@ type DashboardsAPIBuilder struct { func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, dashboardService dashboards.DashboardService, - dashboardVersionService dashver.Service, accessControl accesscontrol.AccessControl, provisioning provisioning.ProvisioningService, dashStore dashboards.Store, reg prometheus.Registerer, sql db.DB, + tracing *tracing.TracingService, ) *DashboardsAPIBuilder { if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { return nil // skip registration unless opting into experimental apis @@ -60,34 +61,63 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, namespacer := request.GetNamespaceMapper(cfg) builder := &DashboardsAPIBuilder{ - dashboardService: dashboardService, - dashboardVersionService: dashboardVersionService, - dashStore: dashStore, - accessControl: accessControl, - namespacer: namespacer, - access: access.NewDashboardAccess(sql, namespacer, dashStore, provisioning), - log: log.New("grafana-apiserver.dashboards"), + log: log.New("grafana-apiserver.dashboards"), + + dashboardService: dashboardService, + accessControl: accessControl, + + legacy: &dashboardStorage{ + resource: dashboard.DashboardResourceInfo, + access: legacy.NewDashboardAccess(sql, namespacer, dashStore, provisioning), + tableConverter: gapiutil.NewTableConverter( + dashboard.DashboardResourceInfo.GroupResource(), + []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Title", Type: "string", Format: "string", Description: "The dashboard name"}, + {Name: "Created At", Type: "date"}, + }, + func(obj any) ([]interface{}, error) { + dash, ok := obj.(*dashboard.Dashboard) + if ok { + if dash != nil { + return []interface{}{ + dash.Name, + dash.Spec.GetNestedString("title"), + dash.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + } + return nil, fmt.Errorf("expected dashboard or summary") + }), + }, } apiregistration.RegisterAPI(builder) return builder } func (b *DashboardsAPIBuilder) GetGroupVersion() schema.GroupVersion { - return v0alpha1.DashboardResourceInfo.GroupVersion() + return dashboard.DashboardResourceInfo.GroupVersion() +} + +func (b *DashboardsAPIBuilder) GetDesiredDualWriterMode(dualWrite bool, modeMap map[string]grafanarest.DualWriterMode) grafanarest.DualWriterMode { + // Add required configuration support in order to enable other modes. For an example, see pkg/registry/apis/playlist/register.go + return grafanarest.Mode0 } func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { scheme.AddKnownTypes(gv, - &v0alpha1.Dashboard{}, - &v0alpha1.DashboardList{}, - &v0alpha1.DashboardWithAccessInfo{}, - &v0alpha1.DashboardVersionList{}, - &v0alpha1.VersionsQueryOptions{}, + &dashboard.Dashboard{}, + &dashboard.DashboardList{}, + &dashboard.DashboardWithAccessInfo{}, + &dashboard.DashboardVersionList{}, + &dashboard.VersionsQueryOptions{}, + &metav1.PartialObjectMetadata{}, + &metav1.PartialObjectMetadataList{}, ) } func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { - resourceInfo := v0alpha1.DashboardResourceInfo + resourceInfo := dashboard.DashboardResourceInfo addKnownTypes(scheme, resourceInfo.GroupVersion()) // Link this version to the internal representation. @@ -112,47 +142,47 @@ func (b *DashboardsAPIBuilder) GetAPIGroupInfo( optsGetter generic.RESTOptionsGetter, dualWriteBuilder grafanarest.DualWriteBuilder, ) (*genericapiserver.APIGroupInfo, error) { - apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(v0alpha1.GROUP, scheme, metav1.ParameterCodec, codecs) + apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(dashboard.GROUP, scheme, metav1.ParameterCodec, codecs) - resourceInfo := v0alpha1.DashboardResourceInfo - store, err := newStorage(scheme) + dash := b.legacy.resource + legacyStore, err := b.legacy.newStore(scheme, optsGetter) if err != nil { return nil, err } - legacyStore := &dashboardStorage{ - resource: resourceInfo, - access: b.access, - tableConverter: store.TableConvertor, - } - storage := map[string]rest.Storage{} - storage[resourceInfo.StoragePath()] = legacyStore - storage[resourceInfo.StoragePath("dto")] = &DTOConnector{ - builder: b, - } - storage[resourceInfo.StoragePath("versions")] = &VersionsREST{ + storage[dash.StoragePath()] = legacyStore + storage[dash.StoragePath("dto")] = &DTOConnector{ builder: b, } + storage[dash.StoragePath("history")] = apistore.NewHistoryConnector( + b.legacy.server, // as client??? + dashboard.DashboardResourceInfo.GroupResource(), + ) // Dual writes if a RESTOptionsGetter is provided if optsGetter != nil && dualWriteBuilder != nil { + store, err := newStorage(scheme) + if err != nil { + return nil, err + } + options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs} if err := store.CompleteWithOptions(options); err != nil { return nil, err } - storage[resourceInfo.StoragePath()], err = dualWriteBuilder(resourceInfo.GroupResource(), legacyStore, store) + storage[dash.StoragePath()], err = dualWriteBuilder(dash.GroupResource(), legacyStore, store) if err != nil { return nil, err } } - apiGroupInfo.VersionedResourcesStorageMap[v0alpha1.VERSION] = storage + apiGroupInfo.VersionedResourcesStorageMap[dashboard.VERSION] = storage return &apiGroupInfo, nil } func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - return v0alpha1.GetOpenAPIDefinitions + return dashboard.GetOpenAPIDefinitions } func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { @@ -163,8 +193,8 @@ func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op root := "/apis/" + b.GetGroupVersion().String() + "/" // Hide the ability to list or watch across all tenants - delete(oas.Paths.Paths, root+v0alpha1.DashboardResourceInfo.GroupResource().Resource) - delete(oas.Paths.Paths, root+"watch/"+v0alpha1.DashboardResourceInfo.GroupResource().Resource) + delete(oas.Paths.Paths, root+dashboard.DashboardResourceInfo.GroupResource().Resource) + delete(oas.Paths.Paths, root+"watch/"+dashboard.DashboardResourceInfo.GroupResource().Resource) // The root API discovery list sub := oas.Paths.Paths[root] diff --git a/pkg/registry/apis/dashboard/sub_dto.go b/pkg/registry/apis/dashboard/sub_dto.go index 3a5a86cba94..7bc53b23724 100644 --- a/pkg/registry/apis/dashboard/sub_dto.go +++ b/pkg/registry/apis/dashboard/sub_dto.go @@ -2,6 +2,7 @@ package dashboard import ( "context" + "encoding/json" "fmt" "net/http" @@ -9,6 +10,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/slugify" @@ -16,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/storage/unified/resource" ) // The DTO returns everything the UI needs in a single request @@ -23,8 +26,10 @@ type DTOConnector struct { builder *DashboardsAPIBuilder } -var _ = rest.Connecter(&DTOConnector{}) -var _ = rest.StorageMetadata(&DTOConnector{}) +var ( + _ rest.Connecter = (*DTOConnector)(nil) + _ rest.StorageMetadata = (*DTOConnector)(nil) +) func (r *DTOConnector) New() runtime.Object { return &dashboard.DashboardWithAccessInfo{} @@ -88,10 +93,32 @@ func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Ob r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Dashboard, accesscontrol.ScopeAnnotationsTypeDashboard) r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Organization, accesscontrol.ScopeAnnotationsTypeOrganization) - dash, err := r.builder.access.GetDashboard(ctx, info.OrgID, name) + key := &resource.ResourceKey{ + Namespace: info.Value, + Group: dashboard.GROUP, + Resource: dashboard.DashboardResourceInfo.GroupResource().Resource, + Name: name, + } + store := r.builder.legacy.access + rsp, err := store.Read(ctx, &resource.ReadRequest{Key: key}) if err != nil { return nil, err } + dash := &dashboard.Dashboard{} + err = json.Unmarshal(rsp.Value, dash) + if err != nil { + return nil, err + } + + obj, err := utils.MetaAccessor(dash) + if err != nil { + return nil, err + } + blobInfo := obj.GetBlob() + if blobInfo != nil { + fmt.Printf("TODO, load full blob from storage %+v\n", blobInfo) + } + access.Slug = slugify.Slugify(dash.Spec.GetNestedString("title")) access.Url = dashboards.GetDashboardFolderURL(false, name, access.Slug) diff --git a/pkg/registry/apis/dashboard/sub_versions.go b/pkg/registry/apis/dashboard/sub_versions.go deleted file mode 100644 index 4787c5387df..00000000000 --- a/pkg/registry/apis/dashboard/sub_versions.go +++ /dev/null @@ -1,117 +0,0 @@ -package dashboard - -import ( - "context" - "fmt" - "net/http" - "strconv" - "strings" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/rest" - - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - dashver "github.com/grafana/grafana/pkg/services/dashboardversion" -) - -type VersionsREST struct { - builder *DashboardsAPIBuilder -} - -var _ = rest.Connecter(&VersionsREST{}) -var _ = rest.StorageMetadata(&VersionsREST{}) - -func (r *VersionsREST) New() runtime.Object { - return &dashboard.DashboardVersionList{} -} - -func (r *VersionsREST) Destroy() { -} - -func (r *VersionsREST) ConnectMethods() []string { - return []string{"GET"} -} - -func (r *VersionsREST) ProducesMIMETypes(verb string) []string { - return nil -} - -func (r *VersionsREST) ProducesObject(verb string) interface{} { - return &dashboard.DashboardVersionList{} -} - -func (r *VersionsREST) NewConnectOptions() (runtime.Object, bool, string) { - return nil, true, "" -} - -func (r *VersionsREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - path := req.URL.Path - idx := strings.LastIndex(path, "/versions/") - if idx > 0 { - key := path[strings.LastIndex(path, "/")+1:] - version, err := strconv.Atoi(key) - if err != nil { - responder.Error(err) - return - } - - dto, err := r.builder.dashboardVersionService.Get(ctx, &dashver.GetDashboardVersionQuery{ - DashboardUID: uid, - OrgID: info.OrgID, - Version: version, - }) - if err != nil { - responder.Error(err) - return - } - - data, _ := dto.Data.Map() - - // Convert the version to a regular dashboard - dash := &dashboard.Dashboard{ - ObjectMeta: metav1.ObjectMeta{ - Name: uid, - CreationTimestamp: metav1.NewTime(dto.Created), - }, - Spec: common.Unstructured{Object: data}, - } - responder.Object(100, dash) - return - } - - // Or list versions - rsp, err := r.builder.dashboardVersionService.List(ctx, &dashver.ListDashboardVersionsQuery{ - DashboardUID: uid, - OrgID: info.OrgID, - }) - if err != nil { - responder.Error(err) - return - } - versions := &dashboard.DashboardVersionList{} - for _, v := range rsp { - info := dashboard.DashboardVersionInfo{ - Version: v.Version, - Created: v.Created.UnixMilli(), - Message: v.Message, - } - if v.ParentVersion != v.Version { - info.ParentVersion = v.ParentVersion - } - if v.CreatedBy > 0 { - info.CreatedBy = fmt.Sprintf("%d", v.CreatedBy) - } - versions.Items = append(versions.Items, info) - } - responder.Object(http.StatusOK, versions) - }), nil -} diff --git a/pkg/storage/unified/apistore/history.go b/pkg/storage/unified/apistore/history.go new file mode 100644 index 00000000000..9edb5114ef5 --- /dev/null +++ b/pkg/storage/unified/apistore/history.go @@ -0,0 +1,103 @@ +package apistore + +import ( + "context" + "encoding/json" + "net/http" + "strconv" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +type HistoryConnector interface { + rest.Storage + rest.Connecter + rest.StorageMetadata +} + +func NewHistoryConnector(index resource.ResourceIndexServer, gr schema.GroupResource) HistoryConnector { + return &historyREST{ + index: index, + gr: gr, + } +} + +type historyREST struct { + index resource.ResourceIndexServer // should be a client! + gr schema.GroupResource +} + +func (r *historyREST) New() runtime.Object { + return &metav1.PartialObjectMetadataList{} +} + +func (r *historyREST) Destroy() { +} + +func (r *historyREST) ConnectMethods() []string { + return []string{"GET"} +} + +func (r *historyREST) ProducesMIMETypes(verb string) []string { + return nil +} + +func (r *historyREST) ProducesObject(verb string) interface{} { + return &metav1.PartialObjectMetadataList{} +} + +func (r *historyREST) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +func (r *historyREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + key := &resource.ResourceKey{ + Namespace: info.Value, + Group: r.gr.Group, + Resource: r.gr.Resource, + Name: uid, + } + + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + query := req.URL.Query() + rsp, err := r.index.History(ctx, &resource.HistoryRequest{ + NextPageToken: query.Get("token"), + Limit: 100, // TODO, from query + Key: key, + }) + if err != nil { + responder.Error(err) + return + } + + list := &metav1.PartialObjectMetadataList{ + ListMeta: metav1.ListMeta{ + Continue: rsp.NextPageToken, + }, + } + if rsp.ResourceVersion > 0 { + list.ResourceVersion = strconv.FormatInt(rsp.ResourceVersion, 10) + } + for _, v := range rsp.Items { + partial := metav1.PartialObjectMetadata{} + err = json.Unmarshal(v.PartialObjectMeta, &partial) + if err != nil { + responder.Error(err) + return + } + list.Items = append(list.Items, partial) + } + responder.Object(http.StatusOK, list) + }), nil +} diff --git a/pkg/storage/unified/resource/noop.go b/pkg/storage/unified/resource/noop.go index 92778817ba1..633f66d88e7 100644 --- a/pkg/storage/unified/resource/noop.go +++ b/pkg/storage/unified/resource/noop.go @@ -5,8 +5,9 @@ import ( ) var ( - _ DiagnosticsServer = &noopService{} - _ LifecycleHooks = &noopService{} + _ DiagnosticsServer = (*noopService)(nil) + _ ResourceIndexServer = (*noopService)(nil) + _ LifecycleHooks = (*noopService)(nil) ) // noopService is a helper implementation to simplify tests @@ -39,3 +40,13 @@ func (n *noopService) Read(context.Context, *ReadRequest) (*ReadResponse, error) func (n *noopService) List(context.Context, *ListRequest) (*ListResponse, error) { return nil, ErrNotImplementedYet } + +// History implements ResourceServer. +func (n *noopService) History(context.Context, *HistoryRequest) (*HistoryResponse, error) { + return nil, ErrNotImplementedYet +} + +// Origin implements ResourceServer. +func (n *noopService) Origin(context.Context, *OriginRequest) (*OriginResponse, error) { + return nil, ErrNotImplementedYet +} diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go index 8068b0f2fdd..8abaf33b9bb 100644 --- a/pkg/storage/unified/resource/resource.pb.go +++ b/pkg/storage/unified/resource/resource.pb.go @@ -173,7 +173,7 @@ func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber { // Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead. func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{21, 0} + return file_resource_proto_rawDescGZIP(), []int{26, 0} } type ResourceKey struct { @@ -1605,6 +1605,388 @@ func (x *WatchEvent) GetPrevious() *WatchEvent_Resource { return nil } +type HistoryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Starting from the requested page (other query parameters must match!) + NextPageToken string `protobuf:"bytes,1,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // Maximum number of items to return + Limit int64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + // Resource identifier + Key *ResourceKey `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` + // List the deleted values (eg, show trash) + ShowDeleted bool `protobuf:"varint,4,opt,name=show_deleted,json=showDeleted,proto3" json:"show_deleted,omitempty"` +} + +func (x *HistoryRequest) Reset() { + *x = HistoryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HistoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HistoryRequest) ProtoMessage() {} + +func (x *HistoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HistoryRequest.ProtoReflect.Descriptor instead. +func (*HistoryRequest) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{20} +} + +func (x *HistoryRequest) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *HistoryRequest) GetLimit() int64 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *HistoryRequest) GetKey() *ResourceKey { + if x != nil { + return x.Key + } + return nil +} + +func (x *HistoryRequest) GetShowDeleted() bool { + if x != nil { + return x.ShowDeleted + } + return false +} + +type HistoryResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Items []*ResourceMeta `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + // More results exist... pass this in the next request + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // ResourceVersion of the list response + ResourceVersion int64 `protobuf:"varint,3,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` +} + +func (x *HistoryResponse) Reset() { + *x = HistoryResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HistoryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HistoryResponse) ProtoMessage() {} + +func (x *HistoryResponse) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[21] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HistoryResponse.ProtoReflect.Descriptor instead. +func (*HistoryResponse) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{21} +} + +func (x *HistoryResponse) GetItems() []*ResourceMeta { + if x != nil { + return x.Items + } + return nil +} + +func (x *HistoryResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *HistoryResponse) GetResourceVersion() int64 { + if x != nil { + return x.ResourceVersion + } + return 0 +} + +type OriginRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Starting from the requested page (other query parameters must match!) + NextPageToken string `protobuf:"bytes,1,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // Maximum number of items to return + Limit int64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + // Resource identifier + Key *ResourceKey `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` + // List the deleted values (eg, show trash) + Origin string `protobuf:"bytes,4,opt,name=origin,proto3" json:"origin,omitempty"` +} + +func (x *OriginRequest) Reset() { + *x = OriginRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OriginRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OriginRequest) ProtoMessage() {} + +func (x *OriginRequest) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[22] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OriginRequest.ProtoReflect.Descriptor instead. +func (*OriginRequest) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{22} +} + +func (x *OriginRequest) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *OriginRequest) GetLimit() int64 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *OriginRequest) GetKey() *ResourceKey { + if x != nil { + return x.Key + } + return nil +} + +func (x *OriginRequest) GetOrigin() string { + if x != nil { + return x.Origin + } + return "" +} + +type ResourceOriginInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The resource + Key *ResourceKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + // Size of the full resource body + ResourceSize int32 `protobuf:"varint,2,opt,name=resource_size,json=resourceSize,proto3" json:"resource_size,omitempty"` + // Hash for the resource + ResourceHash string `protobuf:"bytes,3,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"` + // The origin name + Origin string `protobuf:"bytes,4,opt,name=origin,proto3" json:"origin,omitempty"` + // Path on the origin + Path string `protobuf:"bytes,5,opt,name=path,proto3" json:"path,omitempty"` + // Verification hash from the origin + Hash string `protobuf:"bytes,6,opt,name=hash,proto3" json:"hash,omitempty"` + // Change time from the origin + Timestamp int64 `protobuf:"varint,7,opt,name=timestamp,proto3" json:"timestamp,omitempty"` +} + +func (x *ResourceOriginInfo) Reset() { + *x = ResourceOriginInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ResourceOriginInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceOriginInfo) ProtoMessage() {} + +func (x *ResourceOriginInfo) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[23] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceOriginInfo.ProtoReflect.Descriptor instead. +func (*ResourceOriginInfo) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{23} +} + +func (x *ResourceOriginInfo) GetKey() *ResourceKey { + if x != nil { + return x.Key + } + return nil +} + +func (x *ResourceOriginInfo) GetResourceSize() int32 { + if x != nil { + return x.ResourceSize + } + return 0 +} + +func (x *ResourceOriginInfo) GetResourceHash() string { + if x != nil { + return x.ResourceHash + } + return "" +} + +func (x *ResourceOriginInfo) GetOrigin() string { + if x != nil { + return x.Origin + } + return "" +} + +func (x *ResourceOriginInfo) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *ResourceOriginInfo) GetHash() string { + if x != nil { + return x.Hash + } + return "" +} + +func (x *ResourceOriginInfo) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +type OriginResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Items []*ResourceOriginInfo `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + // More results exist... pass this in the next request + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // ResourceVersion of the list response + ResourceVersion int64 `protobuf:"varint,3,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` +} + +func (x *OriginResponse) Reset() { + *x = OriginResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OriginResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OriginResponse) ProtoMessage() {} + +func (x *OriginResponse) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[24] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OriginResponse.ProtoReflect.Descriptor instead. +func (*OriginResponse) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{24} +} + +func (x *OriginResponse) GetItems() []*ResourceOriginInfo { + if x != nil { + return x.Items + } + return nil +} + +func (x *OriginResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *OriginResponse) GetResourceVersion() int64 { + if x != nil { + return x.ResourceVersion + } + return 0 +} + type HealthCheckRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1616,7 +1998,7 @@ type HealthCheckRequest struct { func (x *HealthCheckRequest) Reset() { *x = HealthCheckRequest{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[20] + mi := &file_resource_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1629,7 +2011,7 @@ func (x *HealthCheckRequest) String() string { func (*HealthCheckRequest) ProtoMessage() {} func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[20] + mi := &file_resource_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1642,7 +2024,7 @@ func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead. func (*HealthCheckRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{20} + return file_resource_proto_rawDescGZIP(), []int{25} } func (x *HealthCheckRequest) GetService() string { @@ -1663,7 +2045,7 @@ type HealthCheckResponse struct { func (x *HealthCheckResponse) Reset() { *x = HealthCheckResponse{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[21] + mi := &file_resource_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1676,7 +2058,7 @@ func (x *HealthCheckResponse) String() string { func (*HealthCheckResponse) ProtoMessage() {} func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[21] + mi := &file_resource_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1689,7 +2071,7 @@ func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead. func (*HealthCheckResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{21} + return file_resource_proto_rawDescGZIP(), []int{26} } func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus { @@ -1711,7 +2093,7 @@ type WatchEvent_Resource struct { func (x *WatchEvent_Resource) Reset() { *x = WatchEvent_Resource{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[22] + mi := &file_resource_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1724,7 +2106,7 @@ func (x *WatchEvent_Resource) String() string { func (*WatchEvent_Resource) ProtoMessage() {} func (x *WatchEvent_Resource) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[22] + mi := &file_resource_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1936,56 +2318,121 @@ var file_resource_proto_rawDesc = []byte{ 0x08, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d, 0x41, 0x52, 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, - 0x05, 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, - 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, - 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, - 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, - 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x2a, - 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, - 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, - 0x63, 0x74, 0x10, 0x01, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, - 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, - 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x30, 0x01, 0x32, 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, - 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, - 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, + 0x05, 0x22, 0x9a, 0x01, 0x0a, 0x0e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, + 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, + 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, + 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, + 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73, + 0x68, 0x6f, 0x77, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x92, + 0x01, 0x0a, 0x0f, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, + 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, + 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x22, 0x8e, 0x01, 0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, + 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, + 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, + 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, + 0x69, 0x67, 0x69, 0x6e, 0x22, 0xe5, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x27, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, + 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, + 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c, + 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x97, 0x01, 0x0a, + 0x0e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x69, 0x74, + 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, + 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, + 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, - 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, - 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, - 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, + 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, + 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, + 0x57, 0x4e, 0x10, 0x03, 0x2a, 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, + 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, + 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, + 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, + 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, + 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0xc3, 0x01, 0x0a, 0x0d, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x35, 0x0a, 0x04, 0x52, + 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, + 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, + 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, + 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2001,7 +2448,7 @@ func file_resource_proto_rawDescGZIP() []byte { } var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 28) var file_resource_proto_goTypes = []any{ (ResourceVersionMatch)(0), // 0: resource.ResourceVersionMatch (WatchEvent_Type)(0), // 1: resource.WatchEvent.Type @@ -2026,9 +2473,14 @@ var file_resource_proto_goTypes = []any{ (*ListResponse)(nil), // 20: resource.ListResponse (*WatchRequest)(nil), // 21: resource.WatchRequest (*WatchEvent)(nil), // 22: resource.WatchEvent - (*HealthCheckRequest)(nil), // 23: resource.HealthCheckRequest - (*HealthCheckResponse)(nil), // 24: resource.HealthCheckResponse - (*WatchEvent_Resource)(nil), // 25: resource.WatchEvent.Resource + (*HistoryRequest)(nil), // 23: resource.HistoryRequest + (*HistoryResponse)(nil), // 24: resource.HistoryResponse + (*OriginRequest)(nil), // 25: resource.OriginRequest + (*ResourceOriginInfo)(nil), // 26: resource.ResourceOriginInfo + (*OriginResponse)(nil), // 27: resource.OriginResponse + (*HealthCheckRequest)(nil), // 28: resource.HealthCheckRequest + (*HealthCheckResponse)(nil), // 29: resource.HealthCheckResponse + (*WatchEvent_Resource)(nil), // 30: resource.WatchEvent.Resource } var file_resource_proto_depIdxs = []int32{ 7, // 0: resource.ErrorResult.details:type_name -> resource.ErrorDetails @@ -2049,28 +2501,39 @@ var file_resource_proto_depIdxs = []int32{ 4, // 15: resource.ListResponse.items:type_name -> resource.ResourceWrapper 18, // 16: resource.WatchRequest.options:type_name -> resource.ListOptions 1, // 17: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type - 25, // 18: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource - 25, // 19: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource - 2, // 20: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus - 15, // 21: resource.ResourceStore.Read:input_type -> resource.ReadRequest - 9, // 22: resource.ResourceStore.Create:input_type -> resource.CreateRequest - 11, // 23: resource.ResourceStore.Update:input_type -> resource.UpdateRequest - 13, // 24: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest - 19, // 25: resource.ResourceStore.List:input_type -> resource.ListRequest - 21, // 26: resource.ResourceStore.Watch:input_type -> resource.WatchRequest - 23, // 27: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest - 16, // 28: resource.ResourceStore.Read:output_type -> resource.ReadResponse - 10, // 29: resource.ResourceStore.Create:output_type -> resource.CreateResponse - 12, // 30: resource.ResourceStore.Update:output_type -> resource.UpdateResponse - 14, // 31: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse - 20, // 32: resource.ResourceStore.List:output_type -> resource.ListResponse - 22, // 33: resource.ResourceStore.Watch:output_type -> resource.WatchEvent - 24, // 34: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse - 28, // [28:35] is the sub-list for method output_type - 21, // [21:28] is the sub-list for method input_type - 21, // [21:21] is the sub-list for extension type_name - 21, // [21:21] is the sub-list for extension extendee - 0, // [0:21] is the sub-list for field type_name + 30, // 18: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource + 30, // 19: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource + 3, // 20: resource.HistoryRequest.key:type_name -> resource.ResourceKey + 5, // 21: resource.HistoryResponse.items:type_name -> resource.ResourceMeta + 3, // 22: resource.OriginRequest.key:type_name -> resource.ResourceKey + 3, // 23: resource.ResourceOriginInfo.key:type_name -> resource.ResourceKey + 26, // 24: resource.OriginResponse.items:type_name -> resource.ResourceOriginInfo + 2, // 25: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus + 15, // 26: resource.ResourceStore.Read:input_type -> resource.ReadRequest + 9, // 27: resource.ResourceStore.Create:input_type -> resource.CreateRequest + 11, // 28: resource.ResourceStore.Update:input_type -> resource.UpdateRequest + 13, // 29: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest + 19, // 30: resource.ResourceStore.List:input_type -> resource.ListRequest + 21, // 31: resource.ResourceStore.Watch:input_type -> resource.WatchRequest + 15, // 32: resource.ResourceIndex.Read:input_type -> resource.ReadRequest + 23, // 33: resource.ResourceIndex.History:input_type -> resource.HistoryRequest + 25, // 34: resource.ResourceIndex.Origin:input_type -> resource.OriginRequest + 28, // 35: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest + 16, // 36: resource.ResourceStore.Read:output_type -> resource.ReadResponse + 10, // 37: resource.ResourceStore.Create:output_type -> resource.CreateResponse + 12, // 38: resource.ResourceStore.Update:output_type -> resource.UpdateResponse + 14, // 39: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse + 20, // 40: resource.ResourceStore.List:output_type -> resource.ListResponse + 22, // 41: resource.ResourceStore.Watch:output_type -> resource.WatchEvent + 16, // 42: resource.ResourceIndex.Read:output_type -> resource.ReadResponse + 24, // 43: resource.ResourceIndex.History:output_type -> resource.HistoryResponse + 27, // 44: resource.ResourceIndex.Origin:output_type -> resource.OriginResponse + 29, // 45: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse + 36, // [36:46] is the sub-list for method output_type + 26, // [26:36] is the sub-list for method input_type + 26, // [26:26] is the sub-list for extension type_name + 26, // [26:26] is the sub-list for extension extendee + 0, // [0:26] is the sub-list for field type_name } func init() { file_resource_proto_init() } @@ -2320,7 +2783,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[20].Exporter = func(v any, i int) any { - switch v := v.(*HealthCheckRequest); i { + switch v := v.(*HistoryRequest); i { case 0: return &v.state case 1: @@ -2332,7 +2795,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[21].Exporter = func(v any, i int) any { - switch v := v.(*HealthCheckResponse); i { + switch v := v.(*HistoryResponse); i { case 0: return &v.state case 1: @@ -2344,6 +2807,66 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[22].Exporter = func(v any, i int) any { + switch v := v.(*OriginRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_proto_msgTypes[23].Exporter = func(v any, i int) any { + switch v := v.(*ResourceOriginInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_proto_msgTypes[24].Exporter = func(v any, i int) any { + switch v := v.(*OriginResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_proto_msgTypes[25].Exporter = func(v any, i int) any { + switch v := v.(*HealthCheckRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_proto_msgTypes[26].Exporter = func(v any, i int) any { + switch v := v.(*HealthCheckResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_proto_msgTypes[27].Exporter = func(v any, i int) any { switch v := v.(*WatchEvent_Resource); i { case 0: return &v.state @@ -2362,9 +2885,9 @@ func file_resource_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_resource_proto_rawDesc, NumEnums: 3, - NumMessages: 23, + NumMessages: 28, NumExtensions: 0, - NumServices: 2, + NumServices: 3, }, GoTypes: file_resource_proto_goTypes, DependencyIndexes: file_resource_proto_depIdxs, diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto index f99a0ab29e1..988e048a385 100644 --- a/pkg/storage/unified/resource/resource.proto +++ b/pkg/storage/unified/resource/resource.proto @@ -321,6 +321,77 @@ message WatchEvent { Resource previous = 4; } +message HistoryRequest { + // Starting from the requested page (other query parameters must match!) + string next_page_token = 1; + + // Maximum number of items to return + int64 limit = 2; + + // Resource identifier + ResourceKey key = 3; + + // List the deleted values (eg, show trash) + bool show_deleted = 4; +} + +message HistoryResponse { + repeated ResourceMeta items = 1; + + // More results exist... pass this in the next request + string next_page_token = 2; + + // ResourceVersion of the list response + int64 resource_version = 3; +} + +message OriginRequest { + // Starting from the requested page (other query parameters must match!) + string next_page_token = 1; + + // Maximum number of items to return + int64 limit = 2; + + // Resource identifier + ResourceKey key = 3; + + // List the deleted values (eg, show trash) + string origin = 4; +} + +message ResourceOriginInfo { + // The resource + ResourceKey key = 1; + + // Size of the full resource body + int32 resource_size = 2; + + // Hash for the resource + string resource_hash = 3; + + // The origin name + string origin = 4; + + // Path on the origin + string path = 5; + + // Verification hash from the origin + string hash = 6; + + // Change time from the origin + int64 timestamp = 7; +} + +message OriginResponse { + repeated ResourceOriginInfo items = 1; + + // More results exist... pass this in the next request + string next_page_token = 2; + + // ResourceVersion of the list response + int64 resource_version = 3; +} + message HealthCheckRequest { string service = 1; } @@ -357,6 +428,20 @@ service ResourceStore { rpc Watch(WatchRequest) returns (stream WatchEvent); } +// Unlike the ResourceStore, this service can be exposed to clients directly +// It should be implemented with efficient indexes and does not need read-after-write semantics +service ResourceIndex { + // TODO: rpc Search(...) ... eventually a typed response + + rpc Read(ReadRequest) returns (ReadResponse); // Duplicated -- for client read only usage + + // Show resource history (and trash) + rpc History(HistoryRequest) returns (HistoryResponse); + + // Used for efficient provisioning + rpc Origin(OriginRequest) returns (OriginResponse); +} + // Clients can use this service directly // NOTE: This is read only, and no read afer write guarantees service Diagnostics { diff --git a/pkg/storage/unified/resource/resource_grpc.pb.go b/pkg/storage/unified/resource/resource_grpc.pb.go index 17b4d1c4c22..bfd2fa2d52a 100644 --- a/pkg/storage/unified/resource/resource_grpc.pb.go +++ b/pkg/storage/unified/resource/resource_grpc.pb.go @@ -347,6 +347,181 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{ Metadata: "resource.proto", } +const ( + ResourceIndex_Read_FullMethodName = "/resource.ResourceIndex/Read" + ResourceIndex_History_FullMethodName = "/resource.ResourceIndex/History" + ResourceIndex_Origin_FullMethodName = "/resource.ResourceIndex/Origin" +) + +// ResourceIndexClient is the client API for ResourceIndex service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Unlike the ResourceStore, this service can be exposed to clients directly +// It should be implemented with efficient indexes and does not need read-after-write semantics +type ResourceIndexClient interface { + Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) + // Show resource history (and trash) + History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error) + // Used for efficient provisioning + Origin(ctx context.Context, in *OriginRequest, opts ...grpc.CallOption) (*OriginResponse, error) +} + +type resourceIndexClient struct { + cc grpc.ClientConnInterface +} + +func NewResourceIndexClient(cc grpc.ClientConnInterface) ResourceIndexClient { + return &resourceIndexClient{cc} +} + +func (c *resourceIndexClient) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReadResponse) + err := c.cc.Invoke(ctx, ResourceIndex_Read_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceIndexClient) History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HistoryResponse) + err := c.cc.Invoke(ctx, ResourceIndex_History_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceIndexClient) Origin(ctx context.Context, in *OriginRequest, opts ...grpc.CallOption) (*OriginResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OriginResponse) + err := c.cc.Invoke(ctx, ResourceIndex_Origin_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ResourceIndexServer is the server API for ResourceIndex service. +// All implementations should embed UnimplementedResourceIndexServer +// for forward compatibility +// +// Unlike the ResourceStore, this service can be exposed to clients directly +// It should be implemented with efficient indexes and does not need read-after-write semantics +type ResourceIndexServer interface { + Read(context.Context, *ReadRequest) (*ReadResponse, error) + // Show resource history (and trash) + History(context.Context, *HistoryRequest) (*HistoryResponse, error) + // Used for efficient provisioning + Origin(context.Context, *OriginRequest) (*OriginResponse, error) +} + +// UnimplementedResourceIndexServer should be embedded to have forward compatible implementations. +type UnimplementedResourceIndexServer struct { +} + +func (UnimplementedResourceIndexServer) Read(context.Context, *ReadRequest) (*ReadResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Read not implemented") +} +func (UnimplementedResourceIndexServer) History(context.Context, *HistoryRequest) (*HistoryResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method History not implemented") +} +func (UnimplementedResourceIndexServer) Origin(context.Context, *OriginRequest) (*OriginResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Origin not implemented") +} + +// UnsafeResourceIndexServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ResourceIndexServer will +// result in compilation errors. +type UnsafeResourceIndexServer interface { + mustEmbedUnimplementedResourceIndexServer() +} + +func RegisterResourceIndexServer(s grpc.ServiceRegistrar, srv ResourceIndexServer) { + s.RegisterService(&ResourceIndex_ServiceDesc, srv) +} + +func _ResourceIndex_Read_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceIndexServer).Read(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceIndex_Read_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceIndexServer).Read(ctx, req.(*ReadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceIndex_History_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HistoryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceIndexServer).History(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceIndex_History_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceIndexServer).History(ctx, req.(*HistoryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceIndex_Origin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OriginRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceIndexServer).Origin(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceIndex_Origin_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceIndexServer).Origin(ctx, req.(*OriginRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ResourceIndex_ServiceDesc is the grpc.ServiceDesc for ResourceIndex service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ResourceIndex_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "resource.ResourceIndex", + HandlerType: (*ResourceIndexServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Read", + Handler: _ResourceIndex_Read_Handler, + }, + { + MethodName: "History", + Handler: _ResourceIndex_History_Handler, + }, + { + MethodName: "Origin", + Handler: _ResourceIndex_Origin_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "resource.proto", +} + const ( Diagnostics_IsHealthy_FullMethodName = "/resource.Diagnostics/IsHealthy" ) diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 8cdd5f6498e..08554aca1ce 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -22,7 +22,7 @@ import ( // Package-level errors. var ( - ErrNotFound = errors.New("entity not found") + ErrNotFound = errors.New("resource not found") ErrOptimisticLockingFailed = errors.New("optimistic locking failed") ErrUserNotFoundInContext = errors.New("user not found in context") ErrUnableToReadResourceJSON = errors.New("unable to read resource json") @@ -32,6 +32,7 @@ var ( // ResourceServer implements all services type ResourceServer interface { ResourceStoreServer + ResourceIndexServer DiagnosticsServer LifecycleHooks } @@ -67,6 +68,9 @@ type ResourceServerOptions struct { // Real storage backend Backend StorageBackend + // Requests based on a search index + Index ResourceIndexServer + // Diagnostics Diagnostics DiagnosticsServer @@ -89,6 +93,9 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { if opts.Backend == nil { return nil, fmt.Errorf("missing Backend implementation") } + if opts.Index == nil { + opts.Index = &noopService{} + } if opts.Diagnostics == nil { opts.Diagnostics = &noopService{} } @@ -110,6 +117,7 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { tracer: opts.Tracer, log: slog.Default().With("logger", "resource-server"), backend: opts.Backend, + index: opts.Index, diagnostics: opts.Diagnostics, access: opts.WriteAccess, lifecycle: opts.Lifecycle, @@ -125,6 +133,7 @@ type server struct { tracer trace.Tracer log *slog.Logger backend StorageBackend + index ResourceIndexServer diagnostics DiagnosticsServer access WriteAccessHooks lifecycle LifecycleHooks @@ -554,6 +563,22 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { } } +// History implements ResourceServer. +func (s *server) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) { + if err := s.Init(); err != nil { + return nil, err + } + return s.index.History(ctx, req) +} + +// Origin implements ResourceServer. +func (s *server) Origin(ctx context.Context, req *OriginRequest) (*OriginResponse, error) { + if err := s.Init(); err != nil { + return nil, err + } + return s.index.Origin(ctx, req) +} + // IsHealthy implements ResourceServer. func (s *server) IsHealthy(ctx context.Context, req *HealthCheckRequest) (*HealthCheckResponse, error) { if err := s.Init(); err != nil { diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index 8ff9569d91e..6ff14ab4fff 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -47,58 +47,59 @@ func TestIntegrationDashboardsApp(t *testing.T) { t.Run("Check discovery client", func(t *testing.T) { disco := helper.GetGroupVersionInfoJSON("dashboard.grafana.app") - //fmt.Printf("%s", string(disco)) + // fmt.Printf("%s", string(disco)) require.JSONEq(t, `[ - { - "freshness": "Current", - "resources": [ - { - "resource": "dashboards", - "responseKind": { - "group": "", - "kind": "Dashboard", - "version": "" - }, - "scope": "Namespaced", - "singularResource": "dashboard", - "subresources": [ - { - "responseKind": { - "group": "", - "kind": "DashboardWithAccessInfo", - "version": "" - }, - "subresource": "dto", - "verbs": [ - "get" - ] - }, - { - "responseKind": { - "group": "", - "kind": "DashboardVersionList", - "version": "" - }, - "subresource": "versions", - "verbs": [ - "get" - ] - } - ], - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update" - ] - } - ], - "version": "v0alpha1" - } - ]`, disco) + { + "freshness": "Current", + "resources": [ + { + "resource": "dashboards", + "responseKind": { + "group": "", + "kind": "Dashboard", + "version": "" + }, + "scope": "Namespaced", + "singularResource": "dashboard", + "subresources": [ + { + "responseKind": { + "group": "", + "kind": "DashboardWithAccessInfo", + "version": "" + }, + "subresource": "dto", + "verbs": [ + "get" + ] + }, + { + "responseKind": { + "group": "", + "kind": "PartialObjectMetadataList", + "version": "" + }, + "subresource": "history", + "verbs": [ + "get" + ] + } + ], + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + } + ], + "version": "v0alpha1" + } +]`, disco) }) }