K8s/Dashboards: Implement dashboards as StorageBackend (#90295)

This commit is contained in:
Ryan McKinley 2024-07-17 10:49:23 -07:00 committed by GitHub
parent 9459e29775
commit f409f8c169
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1757 additions and 629 deletions

View File

@ -15,6 +15,7 @@ const (
NamespaceAnonymous Namespace = "anonymous"
NamespaceRenderService Namespace = "render"
NamespaceAccessPolicy Namespace = "access-policy"
NamespaceProvisioning Namespace = "provisioning"
NamespaceEmpty Namespace = ""
)

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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)")
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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]

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,

View File

@ -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 {

View File

@ -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"
)

View File

@ -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 {

View File

@ -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)
})
}