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

pull/90553/head
Ryan McKinley 12 months ago committed by GitHub
parent 9459e29775
commit f409f8c169
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      pkg/apimachinery/identity/namespace.go
  2. 35
      pkg/registry/apis/dashboard/access/types.go
  3. 12
      pkg/registry/apis/dashboard/legacy/README.md
  4. 245
      pkg/registry/apis/dashboard/legacy/sql_dashboards.go
  5. 329
      pkg/registry/apis/dashboard/legacy/storage.go
  6. 31
      pkg/registry/apis/dashboard/legacy/token.go
  7. 40
      pkg/registry/apis/dashboard/legacy/types.go
  8. 9
      pkg/registry/apis/dashboard/legacy/utils.go
  9. 13
      pkg/registry/apis/dashboard/legacy/utils_test.go
  10. 188
      pkg/registry/apis/dashboard/legacy_storage.go
  11. 112
      pkg/registry/apis/dashboard/register.go
  12. 33
      pkg/registry/apis/dashboard/sub_dto.go
  13. 117
      pkg/registry/apis/dashboard/sub_versions.go
  14. 103
      pkg/storage/unified/apistore/history.go
  15. 15
      pkg/storage/unified/resource/noop.go
  16. 699
      pkg/storage/unified/resource/resource.pb.go
  17. 85
      pkg/storage/unified/resource/resource.proto
  18. 175
      pkg/storage/unified/resource/resource_grpc.pb.go
  19. 27
      pkg/storage/unified/resource/server.go
  20. 7
      pkg/tests/apis/dashboard/dashboards_test.go

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

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

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

@ -1,27 +1,28 @@
package access package legacy
import ( import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings" "sync"
"time" "time"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/utils/ptr"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/sqlstore/session" "github.com/grafana/grafana/pkg/services/sqlstore/session"
"github.com/grafana/grafana/pkg/storage/unified/resource"
) )
var ( var (
@ -29,18 +30,15 @@ var (
) )
type dashboardRow struct { type dashboardRow struct {
// The numeric version for this dashboard
RV int64
// Dashboard resource // Dashboard resource
Dash *dashboardsV0.Dashboard Dash *dashboardsV0.Dashboard
// Title -- this may come from saved metadata rather than the body
Title string
// The folder UID (needed for access control checks) // The folder UID (needed for access control checks)
FolderUID string FolderUID string
// Needed for fast summary access
Tags []string
// Size (in bytes) of the dashboard payload // Size (in bytes) of the dashboard payload
Bytes int Bytes int
@ -55,9 +53,17 @@ type dashboardSqlAccess struct {
namespacer request.NamespaceMapper namespacer request.NamespaceMapper
dashStore dashboards.Store dashStore dashboards.Store
provisioning provisioning.ProvisioningService 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{ return &dashboardSqlAccess{
sql: sql, sql: sql,
sess: sql.GetSqlxSession(), sess: sql.GetSqlxSession(),
@ -69,72 +75,100 @@ func NewDashboardAccess(sql db.DB, namespacer request.NamespaceMapper, dashStore
const selector = `SELECT const selector = `SELECT
dashboard.org_id, dashboard.id, dashboard.org_id, dashboard.id,
dashboard.uid,slug, dashboard.uid, dashboard.folder_uid,
dashboard.folder_uid, dashboard.created,CreatedUSER.uid as created_by,
dashboard.created,dashboard.created_by,CreatedUSER.login, dashboard.updated,UpdatedUSER.uid as updated_by,
dashboard.updated,dashboard.updated_by,UpdatedUSER.login, dashboard.deleted, plugin_id,
plugin_id,
dashboard_provisioning.name as origin_name, dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path, dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key, dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts, dashboard_provisioning.updated as origin_ts,
dashboard.version, dashboard.version, '', dashboard.data
title,
dashboard.data
FROM dashboard FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id 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 CreatedUSER ON dashboard.created_by = CreatedUSER.id
LEFT OUTER JOIN user AS UpdatedUSER ON dashboard.created_by = UpdatedUSER.id LEFT OUTER JOIN user AS UpdatedUSER ON dashboard.updated_by = UpdatedUSER.id
WHERE is_folder = false` WHERE dashboard.is_folder = false`
func (a *dashboardSqlAccess) getRows(ctx context.Context, query *DashboardQuery, onlySummary bool) (*rowsWrapper, int, error) { const history = `SELECT
if !query.Labels.Empty() { dashboard.org_id, dashboard.id,
return nil, 0, fmt.Errorf("label selection not yet supported") dashboard.uid, dashboard.folder_uid,
} dashboard.created,CreatedUSER.uid as created_by,
if len(query.Requirements.SortBy) > 0 { dashboard_version.created,UpdatedUSER.uid as updated_by,
return nil, 0, fmt.Errorf("sorting not yet supported") NULL, plugin_id,
} dashboard_provisioning.name as origin_name,
if query.Requirements.ListHistory != "" { dashboard_provisioning.external_id as origin_path,
return nil, 0, fmt.Errorf("ListHistory not yet supported") dashboard_provisioning.check_sum as origin_key,
} dashboard_provisioning.updated as origin_ts,
if query.Requirements.ListDeleted { dashboard_version.version, dashboard_version.message, dashboard_version.data
return nil, 0, fmt.Errorf("ListDeleted not yet supported") 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`
token, err := readContinueToken(query) func (a *dashboardSqlAccess) getRows(ctx context.Context, query *DashboardQuery) (*rowsWrapper, int, error) {
if err != nil { if len(query.Labels) > 0 {
return nil, 0, err 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))
// }
} }
var sqlcmd string
args := []any{query.OrgID}
limit := query.Limit limit := query.Limit
if limit < 1 { if limit < 1 {
limit = 15 // 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")
}
sqlcmd = fmt.Sprintf("%s AND dashboard.org_id=$%d\n ", history, len(args))
// We can not do this yet because title + tags are in the body if query.UID == "" {
if onlySummary && false { return nil, 0, fmt.Errorf("history query must have a UID")
sqlcmd = strings.Replace(sqlcmd, "dashboard.data", `"{}"`, 1)
} }
sqlcmd = fmt.Sprintf("%s AND dashboard.org_id=$%d", sqlcmd, len(args))
if query.UID != "" {
args = append(args, query.UID) args = append(args, query.UID)
sqlcmd = fmt.Sprintf("%s AND dashboard.uid=$%d", sqlcmd, len(args)) 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 { } else {
args = append(args, token.id) sqlcmd = fmt.Sprintf("%s AND dashboard.org_id=$%d\n ", selector, len(args))
if query.UID != "" {
args = append(args, query.UID)
sqlcmd = fmt.Sprintf("%s AND dashboard.uid=$%d", sqlcmd, len(args))
} else if query.LastID > 0 {
args = append(args, query.LastID)
sqlcmd = fmt.Sprintf("%s AND dashboard.id>=$%d", sqlcmd, len(args)) sqlcmd = fmt.Sprintf("%s AND dashboard.id>=$%d", sqlcmd, len(args))
} }
if query.GetTrash {
if query.Requirements.Folder != nil { sqlcmd = sqlcmd + " AND dashboard.deleted IS NOT NULL"
args = append(args, *query.Requirements.Folder) } else {
sqlcmd = fmt.Sprintf("%s AND dashboard.folder_uid=$%d", sqlcmd, len(args)) sqlcmd = sqlcmd + " AND dashboard.deleted IS NULL"
} }
args = append(args, (limit + 2)) // add more so we can include a next token 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)) 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...) rows, err := a.doQuery(ctx, sqlcmd, args...)
if err != nil { if err != nil {
@ -146,51 +180,8 @@ func (a *dashboardSqlAccess) getRows(ctx context.Context, query *DashboardQuery,
return rows, limit, err 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -199,7 +190,10 @@ func (a *dashboardSqlAccess) doQuery(ctx context.Context, query string, args ...
rows: rows, rows: rows,
a: a, a: a,
// This looks up rules from the permissions on a user // 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 }, err
} }
@ -230,7 +224,6 @@ func (r *rowsWrapper) Next() (*dashboardRow, error) {
if !r.canReadDashboard(scopes...) { if !r.canReadDashboard(scopes...) {
continue continue
} }
d.token.size = r.total // size before next!
r.total += int64(d.Bytes) r.total += int64(d.Bytes)
} }
@ -243,21 +236,20 @@ func (r *rowsWrapper) Next() (*dashboardRow, error) {
func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
dash := &dashboardsV0.Dashboard{ dash := &dashboardsV0.Dashboard{
TypeMeta: dashboardsV0.DashboardResourceInfo.TypeMeta(), TypeMeta: dashboardsV0.DashboardResourceInfo.TypeMeta(),
ObjectMeta: v1.ObjectMeta{Annotations: make(map[string]string)}, ObjectMeta: metav1.ObjectMeta{Annotations: make(map[string]string)},
} }
row := &dashboardRow{Dash: dash} row := &dashboardRow{Dash: dash}
var dashboard_id int64 var dashboard_id int64
var orgId int64 var orgId int64
var slug string
var folder_uid sql.NullString var folder_uid sql.NullString
var updated time.Time var updated time.Time
var updatedByID int64 var updatedBy sql.NullString
var updatedByName sql.NullString var deleted sql.NullTime
var created time.Time var created time.Time
var createdByID int64 var createdBy sql.NullString
var createdByName sql.NullString var message sql.NullString
var plugin_id string var plugin_id string
var origin_name sql.NullString 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 data []byte // the dashboard JSON
var version int64 var version int64
err := rows.Scan(&orgId, &dashboard_id, &dash.Name, err := rows.Scan(&orgId, &dashboard_id, &dash.Name, &folder_uid,
&slug, &folder_uid, &created, &createdBy,
&created, &createdByID, &createdByName, &updated, &updatedBy,
&updated, &updatedByID, &updatedByName, &deleted, &plugin_id,
&plugin_id,
&origin_name, &origin_path, &origin_hash, &origin_ts, &origin_name, &origin_path, &origin_hash, &origin_ts,
&version, &version, &message, &data,
&row.Title, &data,
) )
row.token = &continueToken{orgId: orgId, id: dashboard_id} row.token = &continueToken{orgId: orgId, id: dashboard_id}
if err == nil { 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.Namespace = a.namespacer(orgId)
dash.UID = gapiutil.CalculateClusterWideUID(dash) dash.UID = gapiutil.CalculateClusterWideUID(dash)
dash.SetCreationTimestamp(v1.NewTime(created)) dash.SetCreationTimestamp(metav1.NewTime(created))
meta, err := utils.MetaAccessor(dash) meta, err := utils.MetaAccessor(dash)
if err != nil { if err != nil {
return nil, err return nil, err
} }
meta.SetUpdatedTimestamp(&updated) meta.SetUpdatedTimestamp(&updated)
meta.SetSlug(slug) meta.SetCreatedBy(getUserID(createdBy))
if createdByID > 0 { meta.SetUpdatedBy(getUserID(updatedBy))
meta.SetCreatedBy(fmt.Sprintf("user:%d/%s", createdByID, createdByName.String))
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) meta.SetFolder(folder_uid.String)
row.FolderUID = folder_uid.String row.FolderUID = folder_uid.String
} }
if origin_name.Valid { if origin_name.String != "" {
ts := time.Unix(origin_ts.Int64, 0) ts := time.Unix(origin_ts.Int64, 0)
resolvedPath := a.provisioning.GetDashboardProvisionerResolvedPath(origin_name.String) resolvedPath := a.provisioning.GetDashboardProvisionerResolvedPath(origin_name.String)
@ -332,16 +326,21 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
return row, err return row, err
} }
dash.Spec.Set("id", dashboard_id) // add it so we can get it from the body later 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 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. // DeleteDashboard implements DashboardAccess.
func (a *dashboardSqlAccess) DeleteDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, bool, error) { 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 { if err != nil {
return nil, false, err return nil, false, err
} }
@ -404,6 +403,6 @@ func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, das
if out != nil { if out != nil {
created = (out.Created.Unix() == out.Updated.Unix()) // and now? 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 return dash, created, err
} }

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

@ -1,27 +1,24 @@
package access package legacy
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"github.com/grafana/grafana/pkg/util"
) )
type continueToken struct { type continueToken struct {
orgId int64 orgId int64
id int64 // the internal id (sort by!) id int64 // the internal id (sort by!)
folder string // from the query folder string // from the query
size int64
} }
func readContinueToken(q *DashboardQuery) (continueToken, error) { func readContinueToken(next string) (continueToken, error) {
var err error var err error
token := continueToken{} token := continueToken{}
if q.ContinueToken == "" { if next == "" {
return token, nil return token, nil
} }
parts := strings.Split(q.ContinueToken, "/") parts := strings.Split(next, "/")
if len(parts) < 3 { if len(parts) < 3 {
return token, fmt.Errorf("invalid continue token (too few parts)") return token, fmt.Errorf("invalid continue token (too few parts)")
} }
@ -49,19 +46,19 @@ func readContinueToken(q *DashboardQuery) (continueToken, error) {
} }
token.folder = sub[1] token.folder = sub[1]
// Check if the folder filter is the same from the previous query // // Check if the folder filter is the same from the previous query
if q.Requirements.Folder == nil { // if q.Requirements.Folder == nil {
if token.folder != "" { // if token.folder != "" {
return token, fmt.Errorf("invalid token, the folder must match previous query") // return token, fmt.Errorf("invalid token, the folder must match previous query")
} // }
} else if token.folder != *q.Requirements.Folder { // } else if token.folder != *q.Requirements.Folder {
return token, fmt.Errorf("invalid token, the folder must match previous query") // return token, fmt.Errorf("invalid token, the folder must match previous query")
} // }
return token, err return token, err
} }
func (r *continueToken) String() string { func (r *continueToken) String() string {
return fmt.Sprintf("org:%d/start:%d/folder:%s/%s", return fmt.Sprintf("org:%d/start:%d/folder:%s",
r.orgId, r.id, r.folder, util.ByteCountSI(r.size)) r.orgId, r.id, r.folder)
} }

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

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

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

@ -1,170 +1,70 @@
package dashboard package dashboard
import ( 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/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/access" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/services/apiserver/storage/entity" "github.com/grafana/grafana/pkg/storage/unified/apistore"
) "github.com/grafana/grafana/pkg/storage/unified/resource"
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)
) )
type dashboardStorage struct { type dashboardStorage struct {
resource common.ResourceInfo resource common.ResourceInfo
access access.DashboardAccess access legacy.DashboardAccess
tableConverter rest.TableConvertor tableConverter rest.TableConvertor
}
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) { server resource.ResourceServer
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
} }
func (s *dashboardStorage) Create(ctx context.Context, func (s *dashboardStorage) newStore(scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter) (grafanarest.LegacyStorage, error) {
obj runtime.Object, server, err := resource.NewResourceServer(resource.ResourceServerOptions{
createValidation rest.ValidateObjectFunc, Backend: s.access,
options *metav1.CreateOptions, Index: s.access,
) (runtime.Object, error) { // WriteAccess: resource.WriteAccessHooks{
info, err := request.NamespaceInfoFrom(ctx, true) // Folder: func(ctx context.Context, user identity.Requester, uid string) bool {
// // ???
// },
// },
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.server = server
p, ok := obj.(*v0alpha1.Dashboard) resourceInfo := s.resource
if !ok { defaultOpts, err := defaultOptsGetter.GetRESTOptions(resourceInfo.GroupResource())
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)
if err != nil { if err != nil {
return nil, err 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{
// translate grafana.app/* label selectors into field requirements NewFunc: resourceInfo.NewFunc,
requirements, newSelector, err := entity.ReadLabelSelectors(options.LabelSelector) NewListFunc: resourceInfo.NewListFunc,
if err != nil { KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
return nil, err KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
} PredicateFunc: grafanaregistry.Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
query := &access.DashboardQuery{ SingularQualifiedResource: resourceInfo.SingularGroupResource(),
OrgID: orgId, CreateStrategy: strategy,
Limit: int(options.Limit), UpdateStrategy: strategy,
MaxBytes: 2 * 1024 * 1024, // 2MB, DeleteStrategy: strategy,
ContinueToken: options.Continue, TableConvertor: s.tableConverter,
Requirements: requirements, }
Labels: newSelector,
} options := &generic.StoreOptions{RESTOptions: optsGetter}
if err := store.CompleteWithOptions(options); err != nil {
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 nil, err
} }
return store, 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")
} }

@ -1,6 +1,10 @@
package dashboard package dashboard
import ( import (
"fmt"
"time"
"github.com/prometheus/client_golang/prometheus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
@ -11,22 +15,22 @@ import (
common "k8s.io/kube-openapi/pkg/common" common "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/spec3"
"github.com/prometheus/client_golang/prometheus" dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log" "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/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "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/dashboards"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
) )
var _ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil) var _ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
@ -35,11 +39,8 @@ var _ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
type DashboardsAPIBuilder struct { type DashboardsAPIBuilder struct {
dashboardService dashboards.DashboardService dashboardService dashboards.DashboardService
dashboardVersionService dashver.Service
accessControl accesscontrol.AccessControl accessControl accesscontrol.AccessControl
namespacer request.NamespaceMapper legacy *dashboardStorage
access access.DashboardAccess
dashStore dashboards.Store
log log.Logger log log.Logger
} }
@ -47,12 +48,12 @@ type DashboardsAPIBuilder struct {
func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar, apiregistration builder.APIRegistrar,
dashboardService dashboards.DashboardService, dashboardService dashboards.DashboardService,
dashboardVersionService dashver.Service,
accessControl accesscontrol.AccessControl, accessControl accesscontrol.AccessControl,
provisioning provisioning.ProvisioningService, provisioning provisioning.ProvisioningService,
dashStore dashboards.Store, dashStore dashboards.Store,
reg prometheus.Registerer, reg prometheus.Registerer,
sql db.DB, sql db.DB,
tracing *tracing.TracingService,
) *DashboardsAPIBuilder { ) *DashboardsAPIBuilder {
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
return nil // skip registration unless opting into experimental apis 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) namespacer := request.GetNamespaceMapper(cfg)
builder := &DashboardsAPIBuilder{ builder := &DashboardsAPIBuilder{
log: log.New("grafana-apiserver.dashboards"),
dashboardService: dashboardService, dashboardService: dashboardService,
dashboardVersionService: dashboardVersionService,
dashStore: dashStore,
accessControl: accessControl, accessControl: accessControl,
namespacer: namespacer,
access: access.NewDashboardAccess(sql, namespacer, dashStore, provisioning), legacy: &dashboardStorage{
log: log.New("grafana-apiserver.dashboards"), 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) apiregistration.RegisterAPI(builder)
return builder return builder
} }
func (b *DashboardsAPIBuilder) GetGroupVersion() schema.GroupVersion { 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) { func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
scheme.AddKnownTypes(gv, scheme.AddKnownTypes(gv,
&v0alpha1.Dashboard{}, &dashboard.Dashboard{},
&v0alpha1.DashboardList{}, &dashboard.DashboardList{},
&v0alpha1.DashboardWithAccessInfo{}, &dashboard.DashboardWithAccessInfo{},
&v0alpha1.DashboardVersionList{}, &dashboard.DashboardVersionList{},
&v0alpha1.VersionsQueryOptions{}, &dashboard.VersionsQueryOptions{},
&metav1.PartialObjectMetadata{},
&metav1.PartialObjectMetadataList{},
) )
} }
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
resourceInfo := v0alpha1.DashboardResourceInfo resourceInfo := dashboard.DashboardResourceInfo
addKnownTypes(scheme, resourceInfo.GroupVersion()) addKnownTypes(scheme, resourceInfo.GroupVersion())
// Link this version to the internal representation. // Link this version to the internal representation.
@ -112,47 +142,47 @@ func (b *DashboardsAPIBuilder) GetAPIGroupInfo(
optsGetter generic.RESTOptionsGetter, optsGetter generic.RESTOptionsGetter,
dualWriteBuilder grafanarest.DualWriteBuilder, dualWriteBuilder grafanarest.DualWriteBuilder,
) (*genericapiserver.APIGroupInfo, error) { ) (*genericapiserver.APIGroupInfo, error) {
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(v0alpha1.GROUP, scheme, metav1.ParameterCodec, codecs) apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(dashboard.GROUP, scheme, metav1.ParameterCodec, codecs)
resourceInfo := v0alpha1.DashboardResourceInfo dash := b.legacy.resource
store, err := newStorage(scheme) legacyStore, err := b.legacy.newStore(scheme, optsGetter)
if err != nil { if err != nil {
return nil, err return nil, err
} }
legacyStore := &dashboardStorage{
resource: resourceInfo,
access: b.access,
tableConverter: store.TableConvertor,
}
storage := map[string]rest.Storage{} storage := map[string]rest.Storage{}
storage[resourceInfo.StoragePath()] = legacyStore storage[dash.StoragePath()] = legacyStore
storage[resourceInfo.StoragePath("dto")] = &DTOConnector{ storage[dash.StoragePath("dto")] = &DTOConnector{
builder: b,
}
storage[resourceInfo.StoragePath("versions")] = &VersionsREST{
builder: b, builder: b,
} }
storage[dash.StoragePath("history")] = apistore.NewHistoryConnector(
b.legacy.server, // as client???
dashboard.DashboardResourceInfo.GroupResource(),
)
// Dual writes if a RESTOptionsGetter is provided // Dual writes if a RESTOptionsGetter is provided
if optsGetter != nil && dualWriteBuilder != nil { if optsGetter != nil && dualWriteBuilder != nil {
store, err := newStorage(scheme)
if err != nil {
return nil, err
}
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs} options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs}
if err := store.CompleteWithOptions(options); err != nil { if err := store.CompleteWithOptions(options); err != nil {
return nil, err return nil, err
} }
storage[resourceInfo.StoragePath()], err = dualWriteBuilder(resourceInfo.GroupResource(), legacyStore, store) storage[dash.StoragePath()], err = dualWriteBuilder(dash.GroupResource(), legacyStore, store)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
apiGroupInfo.VersionedResourcesStorageMap[v0alpha1.VERSION] = storage apiGroupInfo.VersionedResourcesStorageMap[dashboard.VERSION] = storage
return &apiGroupInfo, nil return &apiGroupInfo, nil
} }
func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return v0alpha1.GetOpenAPIDefinitions return dashboard.GetOpenAPIDefinitions
} }
func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { 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() + "/" root := "/apis/" + b.GetGroupVersion().String() + "/"
// Hide the ability to list or watch across all tenants // Hide the ability to list or watch across all tenants
delete(oas.Paths.Paths, root+v0alpha1.DashboardResourceInfo.GroupResource().Resource) delete(oas.Paths.Paths, root+dashboard.DashboardResourceInfo.GroupResource().Resource)
delete(oas.Paths.Paths, root+"watch/"+v0alpha1.DashboardResourceInfo.GroupResource().Resource) delete(oas.Paths.Paths, root+"watch/"+dashboard.DashboardResourceInfo.GroupResource().Resource)
// The root API discovery list // The root API discovery list
sub := oas.Paths.Paths[root] sub := oas.Paths.Paths[root]

@ -2,6 +2,7 @@ package dashboard
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@ -9,6 +10,7 @@ import (
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/infra/slugify" "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/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian" "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 // The DTO returns everything the UI needs in a single request
@ -23,8 +26,10 @@ type DTOConnector struct {
builder *DashboardsAPIBuilder builder *DashboardsAPIBuilder
} }
var _ = rest.Connecter(&DTOConnector{}) var (
var _ = rest.StorageMetadata(&DTOConnector{}) _ rest.Connecter = (*DTOConnector)(nil)
_ rest.StorageMetadata = (*DTOConnector)(nil)
)
func (r *DTOConnector) New() runtime.Object { func (r *DTOConnector) New() runtime.Object {
return &dashboard.DashboardWithAccessInfo{} 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.Dashboard, accesscontrol.ScopeAnnotationsTypeDashboard)
r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Organization, accesscontrol.ScopeAnnotationsTypeOrganization) 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 { if err != nil {
return nil, err 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.Slug = slugify.Slugify(dash.Spec.GetNestedString("title"))
access.Url = dashboards.GetDashboardFolderURL(false, name, access.Slug) access.Url = dashboards.GetDashboardFolderURL(false, name, access.Slug)

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

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

@ -5,8 +5,9 @@ import (
) )
var ( var (
_ DiagnosticsServer = &noopService{} _ DiagnosticsServer = (*noopService)(nil)
_ LifecycleHooks = &noopService{} _ ResourceIndexServer = (*noopService)(nil)
_ LifecycleHooks = (*noopService)(nil)
) )
// noopService is a helper implementation to simplify tests // 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) { func (n *noopService) List(context.Context, *ListRequest) (*ListResponse, error) {
return nil, ErrNotImplementedYet 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
}

@ -173,7 +173,7 @@ func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber {
// Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead. // Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead.
func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) { 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 { type ResourceKey struct {
@ -1605,6 +1605,388 @@ func (x *WatchEvent) GetPrevious() *WatchEvent_Resource {
return nil 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 { type HealthCheckRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@ -1616,7 +1998,7 @@ type HealthCheckRequest struct {
func (x *HealthCheckRequest) Reset() { func (x *HealthCheckRequest) Reset() {
*x = HealthCheckRequest{} *x = HealthCheckRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_resource_proto_msgTypes[20] mi := &file_resource_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1629,7 +2011,7 @@ func (x *HealthCheckRequest) String() string {
func (*HealthCheckRequest) ProtoMessage() {} func (*HealthCheckRequest) ProtoMessage() {}
func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message {
mi := &file_resource_proto_msgTypes[20] mi := &file_resource_proto_msgTypes[25]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1642,7 +2024,7 @@ func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead. // Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead.
func (*HealthCheckRequest) Descriptor() ([]byte, []int) { func (*HealthCheckRequest) Descriptor() ([]byte, []int) {
return file_resource_proto_rawDescGZIP(), []int{20} return file_resource_proto_rawDescGZIP(), []int{25}
} }
func (x *HealthCheckRequest) GetService() string { func (x *HealthCheckRequest) GetService() string {
@ -1663,7 +2045,7 @@ type HealthCheckResponse struct {
func (x *HealthCheckResponse) Reset() { func (x *HealthCheckResponse) Reset() {
*x = HealthCheckResponse{} *x = HealthCheckResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_resource_proto_msgTypes[21] mi := &file_resource_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1676,7 +2058,7 @@ func (x *HealthCheckResponse) String() string {
func (*HealthCheckResponse) ProtoMessage() {} func (*HealthCheckResponse) ProtoMessage() {}
func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message {
mi := &file_resource_proto_msgTypes[21] mi := &file_resource_proto_msgTypes[26]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1689,7 +2071,7 @@ func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead. // Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead.
func (*HealthCheckResponse) Descriptor() ([]byte, []int) { 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 { func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus {
@ -1711,7 +2093,7 @@ type WatchEvent_Resource struct {
func (x *WatchEvent_Resource) Reset() { func (x *WatchEvent_Resource) Reset() {
*x = WatchEvent_Resource{} *x = WatchEvent_Resource{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_resource_proto_msgTypes[22] mi := &file_resource_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1724,7 +2106,7 @@ func (x *WatchEvent_Resource) String() string {
func (*WatchEvent_Resource) ProtoMessage() {} func (*WatchEvent_Resource) ProtoMessage() {}
func (x *WatchEvent_Resource) ProtoReflect() protoreflect.Message { func (x *WatchEvent_Resource) ProtoReflect() protoreflect.Message {
mi := &file_resource_proto_msgTypes[22] mi := &file_resource_proto_msgTypes[27]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { 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, 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, 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, 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, 0x05, 0x22, 0x9a, 0x01, 0x0a, 0x0e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67,
0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e,
0x65, 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05,
0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d,
0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73,
0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x68, 0x6f, 0x77, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28,
0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x08, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x92,
0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x01, 0x0a, 0x0f, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73,
0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x2a, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73,
0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f,
0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50,
0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f,
0x63, 0x74, 0x10, 0x01, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01,
0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73,
0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x69, 0x6f, 0x6e, 0x22, 0x8e, 0x01, 0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61,
0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a,
0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69,
0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f,
0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x69, 0x67, 0x69, 0x6e, 0x22, 0xe5, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x27, 0x0a, 0x03, 0x6b,
0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x73,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73,
0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16,
0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61,
0x6e, 0x74, 0x30, 0x01, 0x32, 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c,
0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28,
0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x97, 0x01, 0x0a,
0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 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, 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, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65,
0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61,
0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74,
0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 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 ( var (
@ -2001,7 +2448,7 @@ func file_resource_proto_rawDescGZIP() []byte {
} }
var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 3) 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{ var file_resource_proto_goTypes = []any{
(ResourceVersionMatch)(0), // 0: resource.ResourceVersionMatch (ResourceVersionMatch)(0), // 0: resource.ResourceVersionMatch
(WatchEvent_Type)(0), // 1: resource.WatchEvent.Type (WatchEvent_Type)(0), // 1: resource.WatchEvent.Type
@ -2026,9 +2473,14 @@ var file_resource_proto_goTypes = []any{
(*ListResponse)(nil), // 20: resource.ListResponse (*ListResponse)(nil), // 20: resource.ListResponse
(*WatchRequest)(nil), // 21: resource.WatchRequest (*WatchRequest)(nil), // 21: resource.WatchRequest
(*WatchEvent)(nil), // 22: resource.WatchEvent (*WatchEvent)(nil), // 22: resource.WatchEvent
(*HealthCheckRequest)(nil), // 23: resource.HealthCheckRequest (*HistoryRequest)(nil), // 23: resource.HistoryRequest
(*HealthCheckResponse)(nil), // 24: resource.HealthCheckResponse (*HistoryResponse)(nil), // 24: resource.HistoryResponse
(*WatchEvent_Resource)(nil), // 25: resource.WatchEvent.Resource (*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{ var file_resource_proto_depIdxs = []int32{
7, // 0: resource.ErrorResult.details:type_name -> resource.ErrorDetails 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 4, // 15: resource.ListResponse.items:type_name -> resource.ResourceWrapper
18, // 16: resource.WatchRequest.options:type_name -> resource.ListOptions 18, // 16: resource.WatchRequest.options:type_name -> resource.ListOptions
1, // 17: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type 1, // 17: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type
25, // 18: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource 30, // 18: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource
25, // 19: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource 30, // 19: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource
2, // 20: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus 3, // 20: resource.HistoryRequest.key:type_name -> resource.ResourceKey
15, // 21: resource.ResourceStore.Read:input_type -> resource.ReadRequest 5, // 21: resource.HistoryResponse.items:type_name -> resource.ResourceMeta
9, // 22: resource.ResourceStore.Create:input_type -> resource.CreateRequest 3, // 22: resource.OriginRequest.key:type_name -> resource.ResourceKey
11, // 23: resource.ResourceStore.Update:input_type -> resource.UpdateRequest 3, // 23: resource.ResourceOriginInfo.key:type_name -> resource.ResourceKey
13, // 24: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest 26, // 24: resource.OriginResponse.items:type_name -> resource.ResourceOriginInfo
19, // 25: resource.ResourceStore.List:input_type -> resource.ListRequest 2, // 25: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus
21, // 26: resource.ResourceStore.Watch:input_type -> resource.WatchRequest 15, // 26: resource.ResourceStore.Read:input_type -> resource.ReadRequest
23, // 27: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest 9, // 27: resource.ResourceStore.Create:input_type -> resource.CreateRequest
16, // 28: resource.ResourceStore.Read:output_type -> resource.ReadResponse 11, // 28: resource.ResourceStore.Update:input_type -> resource.UpdateRequest
10, // 29: resource.ResourceStore.Create:output_type -> resource.CreateResponse 13, // 29: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest
12, // 30: resource.ResourceStore.Update:output_type -> resource.UpdateResponse 19, // 30: resource.ResourceStore.List:input_type -> resource.ListRequest
14, // 31: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse 21, // 31: resource.ResourceStore.Watch:input_type -> resource.WatchRequest
20, // 32: resource.ResourceStore.List:output_type -> resource.ListResponse 15, // 32: resource.ResourceIndex.Read:input_type -> resource.ReadRequest
22, // 33: resource.ResourceStore.Watch:output_type -> resource.WatchEvent 23, // 33: resource.ResourceIndex.History:input_type -> resource.HistoryRequest
24, // 34: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse 25, // 34: resource.ResourceIndex.Origin:input_type -> resource.OriginRequest
28, // [28:35] is the sub-list for method output_type 28, // 35: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest
21, // [21:28] is the sub-list for method input_type 16, // 36: resource.ResourceStore.Read:output_type -> resource.ReadResponse
21, // [21:21] is the sub-list for extension type_name 10, // 37: resource.ResourceStore.Create:output_type -> resource.CreateResponse
21, // [21:21] is the sub-list for extension extendee 12, // 38: resource.ResourceStore.Update:output_type -> resource.UpdateResponse
0, // [0:21] is the sub-list for field type_name 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() } 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 { file_resource_proto_msgTypes[20].Exporter = func(v any, i int) any {
switch v := v.(*HealthCheckRequest); i { switch v := v.(*HistoryRequest); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@ -2332,7 +2795,7 @@ func file_resource_proto_init() {
} }
} }
file_resource_proto_msgTypes[21].Exporter = func(v any, i int) any { file_resource_proto_msgTypes[21].Exporter = func(v any, i int) any {
switch v := v.(*HealthCheckResponse); i { switch v := v.(*HistoryResponse); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@ -2344,6 +2807,66 @@ func file_resource_proto_init() {
} }
} }
file_resource_proto_msgTypes[22].Exporter = func(v any, i int) any { 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 { switch v := v.(*WatchEvent_Resource); i {
case 0: case 0:
return &v.state return &v.state
@ -2362,9 +2885,9 @@ func file_resource_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_resource_proto_rawDesc, RawDescriptor: file_resource_proto_rawDesc,
NumEnums: 3, NumEnums: 3,
NumMessages: 23, NumMessages: 28,
NumExtensions: 0, NumExtensions: 0,
NumServices: 2, NumServices: 3,
}, },
GoTypes: file_resource_proto_goTypes, GoTypes: file_resource_proto_goTypes,
DependencyIndexes: file_resource_proto_depIdxs, DependencyIndexes: file_resource_proto_depIdxs,

@ -321,6 +321,77 @@ message WatchEvent {
Resource previous = 4; 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 { message HealthCheckRequest {
string service = 1; string service = 1;
} }
@ -357,6 +428,20 @@ service ResourceStore {
rpc Watch(WatchRequest) returns (stream WatchEvent); 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 // Clients can use this service directly
// NOTE: This is read only, and no read afer write guarantees // NOTE: This is read only, and no read afer write guarantees
service Diagnostics { service Diagnostics {

@ -347,6 +347,181 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{
Metadata: "resource.proto", 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 ( const (
Diagnostics_IsHealthy_FullMethodName = "/resource.Diagnostics/IsHealthy" Diagnostics_IsHealthy_FullMethodName = "/resource.Diagnostics/IsHealthy"
) )

@ -22,7 +22,7 @@ import (
// Package-level errors. // Package-level errors.
var ( var (
ErrNotFound = errors.New("entity not found") ErrNotFound = errors.New("resource not found")
ErrOptimisticLockingFailed = errors.New("optimistic locking failed") ErrOptimisticLockingFailed = errors.New("optimistic locking failed")
ErrUserNotFoundInContext = errors.New("user not found in context") ErrUserNotFoundInContext = errors.New("user not found in context")
ErrUnableToReadResourceJSON = errors.New("unable to read resource json") ErrUnableToReadResourceJSON = errors.New("unable to read resource json")
@ -32,6 +32,7 @@ var (
// ResourceServer implements all services // ResourceServer implements all services
type ResourceServer interface { type ResourceServer interface {
ResourceStoreServer ResourceStoreServer
ResourceIndexServer
DiagnosticsServer DiagnosticsServer
LifecycleHooks LifecycleHooks
} }
@ -67,6 +68,9 @@ type ResourceServerOptions struct {
// Real storage backend // Real storage backend
Backend StorageBackend Backend StorageBackend
// Requests based on a search index
Index ResourceIndexServer
// Diagnostics // Diagnostics
Diagnostics DiagnosticsServer Diagnostics DiagnosticsServer
@ -89,6 +93,9 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
if opts.Backend == nil { if opts.Backend == nil {
return nil, fmt.Errorf("missing Backend implementation") return nil, fmt.Errorf("missing Backend implementation")
} }
if opts.Index == nil {
opts.Index = &noopService{}
}
if opts.Diagnostics == nil { if opts.Diagnostics == nil {
opts.Diagnostics = &noopService{} opts.Diagnostics = &noopService{}
} }
@ -110,6 +117,7 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
tracer: opts.Tracer, tracer: opts.Tracer,
log: slog.Default().With("logger", "resource-server"), log: slog.Default().With("logger", "resource-server"),
backend: opts.Backend, backend: opts.Backend,
index: opts.Index,
diagnostics: opts.Diagnostics, diagnostics: opts.Diagnostics,
access: opts.WriteAccess, access: opts.WriteAccess,
lifecycle: opts.Lifecycle, lifecycle: opts.Lifecycle,
@ -125,6 +133,7 @@ type server struct {
tracer trace.Tracer tracer trace.Tracer
log *slog.Logger log *slog.Logger
backend StorageBackend backend StorageBackend
index ResourceIndexServer
diagnostics DiagnosticsServer diagnostics DiagnosticsServer
access WriteAccessHooks access WriteAccessHooks
lifecycle LifecycleHooks 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. // IsHealthy implements ResourceServer.
func (s *server) IsHealthy(ctx context.Context, req *HealthCheckRequest) (*HealthCheckResponse, error) { func (s *server) IsHealthy(ctx context.Context, req *HealthCheckRequest) (*HealthCheckResponse, error) {
if err := s.Init(); err != nil { if err := s.Init(); err != nil {

@ -77,10 +77,10 @@ func TestIntegrationDashboardsApp(t *testing.T) {
{ {
"responseKind": { "responseKind": {
"group": "", "group": "",
"kind": "DashboardVersionList", "kind": "PartialObjectMetadataList",
"version": "" "version": ""
}, },
"subresource": "versions", "subresource": "history",
"verbs": [ "verbs": [
"get" "get"
] ]
@ -93,7 +93,8 @@ func TestIntegrationDashboardsApp(t *testing.T) {
"get", "get",
"list", "list",
"patch", "patch",
"update" "update",
"watch"
] ]
} }
], ],

Loading…
Cancel
Save