From b44333730f081d4fa1bc5fe3a4952e63538f55e6 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 13 Jun 2024 14:26:57 +0300 Subject: [PATCH] now with basic test --- pkg/apimachinery/utils/meta.go | 16 +- pkg/storage/unified/resource/event.go | 24 +- pkg/storage/unified/resource/mem.go | 131 +++++ pkg/storage/unified/resource/metrics.go | 41 ++ pkg/storage/unified/resource/resource.pb.go | 517 +++++++++--------- pkg/storage/unified/resource/resource.proto | 18 +- .../unified/resource/resource_grpc.pb.go | 429 ++++++++++----- pkg/storage/unified/resource/validator.go | 256 --------- pkg/storage/unified/resource/writer.go | 330 +++++++++++ pkg/storage/unified/resource/writer_test.go | 103 ++++ pkg/storage/unified/sqlstash/queries_test.go | 62 --- .../unified/sqlstash/sql_storage_server.go | 132 ++--- 12 files changed, 1227 insertions(+), 832 deletions(-) create mode 100644 pkg/storage/unified/resource/mem.go create mode 100644 pkg/storage/unified/resource/metrics.go delete mode 100644 pkg/storage/unified/resource/validator.go create mode 100644 pkg/storage/unified/resource/writer.go create mode 100644 pkg/storage/unified/resource/writer_test.go delete mode 100644 pkg/storage/unified/sqlstash/queries_test.go diff --git a/pkg/apimachinery/utils/meta.go b/pkg/apimachinery/utils/meta.go index ac5079a2a8e..3deab3cda23 100644 --- a/pkg/apimachinery/utils/meta.go +++ b/pkg/apimachinery/utils/meta.go @@ -62,6 +62,8 @@ type GrafanaMetaAccessor interface { SetUpdatedBy(user string) GetFolder() string SetFolder(uid string) + SetAnnotation(key string, val string) + GetSlug() string SetSlug(v string) @@ -111,7 +113,7 @@ func (m *grafanaMetaAccessor) Object() metav1.Object { return m.obj } -func (m *grafanaMetaAccessor) set(key string, val string) { +func (m *grafanaMetaAccessor) SetAnnotation(key string, val string) { anno := m.obj.GetAnnotations() if val == "" { if anno != nil { @@ -148,7 +150,7 @@ func (m *grafanaMetaAccessor) SetUpdatedTimestampMillis(v int64) { t := time.UnixMilli(v) m.SetUpdatedTimestamp(&t) } else { - m.set(AnnoKeyUpdatedTimestamp, "") // will clear the annotation + m.SetAnnotation(AnnoKeyUpdatedTimestamp, "") // will clear the annotation } } @@ -157,7 +159,7 @@ func (m *grafanaMetaAccessor) SetUpdatedTimestamp(v *time.Time) { if v != nil && v.Unix() != 0 { txt = v.UTC().Format(time.RFC3339) } - m.set(AnnoKeyUpdatedTimestamp, txt) + m.SetAnnotation(AnnoKeyUpdatedTimestamp, txt) } func (m *grafanaMetaAccessor) GetCreatedBy() string { @@ -165,7 +167,7 @@ func (m *grafanaMetaAccessor) GetCreatedBy() string { } func (m *grafanaMetaAccessor) SetCreatedBy(user string) { - m.set(AnnoKeyCreatedBy, user) + m.SetAnnotation(AnnoKeyCreatedBy, user) } func (m *grafanaMetaAccessor) GetUpdatedBy() string { @@ -173,7 +175,7 @@ func (m *grafanaMetaAccessor) GetUpdatedBy() string { } func (m *grafanaMetaAccessor) SetUpdatedBy(user string) { - m.set(AnnoKeyUpdatedBy, user) + m.SetAnnotation(AnnoKeyUpdatedBy, user) } func (m *grafanaMetaAccessor) GetFolder() string { @@ -181,7 +183,7 @@ func (m *grafanaMetaAccessor) GetFolder() string { } func (m *grafanaMetaAccessor) SetFolder(uid string) { - m.set(AnnoKeyFolder, uid) + m.SetAnnotation(AnnoKeyFolder, uid) } func (m *grafanaMetaAccessor) GetSlug() string { @@ -189,7 +191,7 @@ func (m *grafanaMetaAccessor) GetSlug() string { } func (m *grafanaMetaAccessor) SetSlug(v string) { - m.set(AnnoKeySlug, v) + m.SetAnnotation(AnnoKeySlug, v) } func (m *grafanaMetaAccessor) SetOriginInfo(info *ResourceOriginInfo) { diff --git a/pkg/storage/unified/resource/event.go b/pkg/storage/unified/resource/event.go index 793196fcd56..4e48d626600 100644 --- a/pkg/storage/unified/resource/event.go +++ b/pkg/storage/unified/resource/event.go @@ -1,9 +1,8 @@ package resource import ( + context "context" "encoding/json" - "fmt" - "net/http" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -20,23 +19,16 @@ type WriteEvent struct { // NOTE, this is never mutated, only parsed and validated Value json.RawMessage + // Access the raw metadata values Object utils.GrafanaMetaAccessor OldObject utils.GrafanaMetaAccessor - // Change metadata + // Optionally link to a + Blob *LinkBlob + + // Change metadata (useful?) FolderChanged bool - - // The status will be populated for any error - Status *StatusResult - Error error } -func (e *WriteEvent) BadRequest(err error, message string, a ...any) *WriteEvent { - e.Error = err - e.Status = &StatusResult{ - Status: "Failure", - Message: fmt.Sprintf(message, a...), - Code: http.StatusBadRequest, - } - return e -} +// A function to write events +type EventAppender = func(context.Context, *WriteEvent) (int64, error) diff --git a/pkg/storage/unified/resource/mem.go b/pkg/storage/unified/resource/mem.go new file mode 100644 index 00000000000..6d96dafdb13 --- /dev/null +++ b/pkg/storage/unified/resource/mem.go @@ -0,0 +1,131 @@ +package resource + +import ( + context "context" + "crypto/sha1" + "encoding/base64" + "sync" + "sync/atomic" +) + +type MemoryStore interface { + Read(context.Context, *ReadRequest) (*ReadResponse, error) + WriteEvent(context.Context, *WriteEvent) (int64, error) +} + +func NewMemoryStore() MemoryStore { + return &memoryStore{ + store: make(map[string]*namespacedResources), + } +} + +type memoryStore struct { + counter atomic.Int64 + mutex sync.RWMutex + + // Key is group+resource + store map[string]*namespacedResources +} + +type namespacedResources struct { + // Lookup by resource name + namespace map[string]*resourceInfo +} + +type resourceInfo struct { + history []resourceValue +} + +type resourceValue struct { + rv int64 + event WriteEvent // saves the whole thing for now + blobHash string +} + +func (s *memoryStore) get(key *ResourceKey) *resourceValue { + s.mutex.RLock() + defer s.mutex.RUnlock() + + found, ok := s.store[key.Group+"/"+key.Resource] + if !ok || found.namespace == nil { + return nil + } + resource, ok := found.namespace[key.Namespace] + if !ok || len(resource.history) < 1 { + return nil + } + if key.ResourceVersion > 0 { + for idx, v := range resource.history { + if v.rv == key.ResourceVersion { + return &resource.history[idx] + } + } + } + latest := resource.history[0] + if latest.event.Operation == ResourceOperation_DELETED { + return nil + } + return &latest // the first one +} + +func (s *memoryStore) Read(_ context.Context, req *ReadRequest) (*ReadResponse, error) { + val := s.get(req.Key) + if val == nil { + return &ReadResponse{ + Status: &StatusResult{ + Status: "Failure", + Reason: "not found", + Code: 404, + }, + }, nil + } + rsp := &ReadResponse{ + ResourceVersion: val.rv, + Value: val.event.Value, + } + if val.event.Blob != nil { + rsp.BlobUrl = "#blob" + } + return rsp, nil +} + +func (s *memoryStore) WriteEvent(_ context.Context, req *WriteEvent) (int64, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + val := resourceValue{ + rv: s.counter.Add(1), + event: *req, + } + if req.Blob != nil { + hasher := sha1.New() + _, err := hasher.Write(req.Blob.Value) + if err != nil { + return 0, err + } + val.blobHash = base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + } + + // Now append the value + key := req.Key + found, ok := s.store[key.Group+"/"+key.Resource] + if !ok { + found = &namespacedResources{} + s.store[key.Group+"/"+key.Resource] = found + } + if found.namespace == nil { + found.namespace = make(map[string]*resourceInfo) + } + + resource, ok := found.namespace[key.Namespace] + if !ok { + resource = &resourceInfo{} + found.namespace[key.Namespace] = resource + } + if resource.history == nil { + resource.history = []resourceValue{val} + } else { + resource.history = append([]resourceValue{val}, resource.history...) + } + return val.rv, nil +} diff --git a/pkg/storage/unified/resource/metrics.go b/pkg/storage/unified/resource/metrics.go new file mode 100644 index 00000000000..be187eba7b3 --- /dev/null +++ b/pkg/storage/unified/resource/metrics.go @@ -0,0 +1,41 @@ +package resource + +import ( + "sync" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + once sync.Once + StorageServerMetrics *StorageApiMetrics +) + +type StorageApiMetrics struct { + OptimisticLockFailed *prometheus.CounterVec +} + +func NewStorageMetrics() *StorageApiMetrics { + once.Do(func() { + StorageServerMetrics = &StorageApiMetrics{ + OptimisticLockFailed: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "resource_storage", + Name: "optimistic_lock_failed", + Help: "count of optimistic locks failed", + }, + []string{"action"}, + ), + } + }) + + return StorageServerMetrics +} + +func (s *StorageApiMetrics) Collect(ch chan<- prometheus.Metric) { + s.OptimisticLockFailed.Collect(ch) +} + +func (s *StorageApiMetrics) Describe(ch chan<- *prometheus.Desc) { + s.OptimisticLockFailed.Describe(ch) +} diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go index b62e26b7819..982af18053a 100644 --- a/pkg/storage/unified/resource/resource.pb.go +++ b/pkg/storage/unified/resource/resource.pb.go @@ -1091,7 +1091,7 @@ func (x *DeleteResponse) GetResourceVersion() int64 { return 0 } -type GetResourceRequest struct { +type ReadRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -1101,8 +1101,8 @@ type GetResourceRequest struct { IgnoreBlob bool `protobuf:"varint,2,opt,name=ignore_blob,json=ignoreBlob,proto3" json:"ignore_blob,omitempty"` } -func (x *GetResourceRequest) Reset() { - *x = GetResourceRequest{} +func (x *ReadRequest) Reset() { + *x = ReadRequest{} if protoimpl.UnsafeEnabled { mi := &file_resource_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1110,13 +1110,13 @@ func (x *GetResourceRequest) Reset() { } } -func (x *GetResourceRequest) String() string { +func (x *ReadRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GetResourceRequest) ProtoMessage() {} +func (*ReadRequest) ProtoMessage() {} -func (x *GetResourceRequest) ProtoReflect() protoreflect.Message { +func (x *ReadRequest) ProtoReflect() protoreflect.Message { mi := &file_resource_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1128,26 +1128,26 @@ func (x *GetResourceRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GetResourceRequest.ProtoReflect.Descriptor instead. -func (*GetResourceRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead. +func (*ReadRequest) Descriptor() ([]byte, []int) { return file_resource_proto_rawDescGZIP(), []int{12} } -func (x *GetResourceRequest) GetKey() *ResourceKey { +func (x *ReadRequest) GetKey() *ResourceKey { if x != nil { return x.Key } return nil } -func (x *GetResourceRequest) GetIgnoreBlob() bool { +func (x *ReadRequest) GetIgnoreBlob() bool { if x != nil { return x.IgnoreBlob } return false } -type GetResourceResponse struct { +type ReadResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -1163,8 +1163,8 @@ type GetResourceResponse struct { BlobUrl string `protobuf:"bytes,4,opt,name=blob_url,json=blobUrl,proto3" json:"blob_url,omitempty"` } -func (x *GetResourceResponse) Reset() { - *x = GetResourceResponse{} +func (x *ReadResponse) Reset() { + *x = ReadResponse{} if protoimpl.UnsafeEnabled { mi := &file_resource_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1172,13 +1172,13 @@ func (x *GetResourceResponse) Reset() { } } -func (x *GetResourceResponse) String() string { +func (x *ReadResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GetResourceResponse) ProtoMessage() {} +func (*ReadResponse) ProtoMessage() {} -func (x *GetResourceResponse) ProtoReflect() protoreflect.Message { +func (x *ReadResponse) ProtoReflect() protoreflect.Message { mi := &file_resource_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1190,33 +1190,33 @@ func (x *GetResourceResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GetResourceResponse.ProtoReflect.Descriptor instead. -func (*GetResourceResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead. +func (*ReadResponse) Descriptor() ([]byte, []int) { return file_resource_proto_rawDescGZIP(), []int{13} } -func (x *GetResourceResponse) GetStatus() *StatusResult { +func (x *ReadResponse) GetStatus() *StatusResult { if x != nil { return x.Status } return nil } -func (x *GetResourceResponse) GetResourceVersion() int64 { +func (x *ReadResponse) GetResourceVersion() int64 { if x != nil { return x.ResourceVersion } return 0 } -func (x *GetResourceResponse) GetValue() []byte { +func (x *ReadResponse) GetValue() []byte { if x != nil { return x.Value } return nil } -func (x *GetResourceResponse) GetBlobUrl() string { +func (x *ReadResponse) GetBlobUrl() string { if x != nil { return x.BlobUrl } @@ -2421,221 +2421,228 @@ var file_resource_proto_rawDesc = []byte{ 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x5e, 0x0a, 0x12, 0x47, 0x65, 0x74, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x67, 0x6e, 0x6f, - 0x72, 0x65, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, - 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x42, 0x6c, 0x6f, 0x62, 0x22, 0xa1, 0x01, 0x0a, 0x13, 0x47, 0x65, - 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x6c, 0x6f, 0x62, 0x55, 0x72, 0x6c, 0x22, 0x39, 0x0a, - 0x0e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x7f, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, - 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x26, 0x0a, 0x04, 0x69, - 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x62, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x69, - 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x53, 0x0a, 0x0b, 0x52, 0x65, 0x71, - 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x6f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x64, - 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x2a, 0x0a, 0x05, - 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x2e, 0x4f, 0x72, 0x64, 0x65, - 0x72, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x1a, 0x0a, 0x05, 0x4f, 0x72, 0x64, 0x65, - 0x72, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x53, 0x43, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x45, - 0x53, 0x43, 0x10, 0x01, 0x22, 0xc2, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, - 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, - 0x6c, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x05, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, - 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x8a, 0x01, 0x0a, 0x0b, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, - 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x12, 0x22, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x0e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x6f, 0x72, 0x74, - 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x22, 0xc4, 0x01, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, - 0x72, 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, 0x12, 0x30, 0x0a, 0x14, 0x72, - 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x61, 0x69, - 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xe2, 0x01, - 0x0a, 0x0c, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, - 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, - 0x69, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, - 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, - 0x0a, 0x13, 0x73, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x65, - 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x73, 0x65, 0x6e, - 0x64, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x32, - 0x0a, 0x15, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x62, 0x6f, - 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x61, - 0x6c, 0x6c, 0x6f, 0x77, 0x57, 0x61, 0x74, 0x63, 0x68, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, - 0x6b, 0x73, 0x22, 0x9b, 0x01, 0x0a, 0x0d, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x12, 0x35, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x52, - 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x70, 0x72, 0x65, - 0x76, 0x69, 0x6f, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, - 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x52, 0x08, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, - 0x22, 0x9a, 0x01, 0x0a, 0x0e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, - 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, - 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, - 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, - 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x57, 0x0a, 0x0b, 0x52, 0x65, 0x61, + 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x5f, 0x62, 0x6c, 0x6f, 0x62, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x42, 0x6c, + 0x6f, 0x62, 0x22, 0x9a, 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x75, 0x72, 0x6c, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x6c, 0x6f, 0x62, 0x55, 0x72, 0x6c, 0x22, + 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, - 0x6f, 0x77, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x92, 0x01, - 0x0a, 0x0f, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, - 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, - 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x22, 0x8e, 0x01, 0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, - 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, - 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, - 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, - 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x6f, - 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, - 0x67, 0x69, 0x6e, 0x22, 0xe5, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, - 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, - 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, - 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, - 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, - 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x97, 0x01, 0x0a, 0x0e, - 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, - 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x7f, 0x0a, 0x0f, 0x47, 0x65, + 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x26, 0x0a, + 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x62, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x53, 0x0a, 0x0b, 0x52, + 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x22, 0x64, 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x2a, + 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x2e, 0x4f, 0x72, + 0x64, 0x65, 0x72, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x1a, 0x0a, 0x05, 0x4f, 0x72, + 0x64, 0x65, 0x72, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x53, 0x43, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, + 0x44, 0x45, 0x53, 0x43, 0x10, 0x01, 0x22, 0xc2, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x6c, 0x61, + 0x62, 0x65, 0x6c, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x66, 0x69, 0x65, + 0x6c, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x8a, 0x01, 0x0a, 0x0b, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, + 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x6f, + 0x72, 0x74, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x22, 0xc4, 0x01, 0x0a, 0x0c, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x05, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, + 0x70, 0x65, 0x72, 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, 0x12, 0x30, 0x0a, + 0x14, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x72, 0x65, 0x6d, + 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, + 0xe2, 0x01, 0x0a, 0x0c, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, + 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x73, + 0x65, 0x6e, 0x64, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, + 0x12, 0x32, 0x0a, 0x15, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x5f, + 0x62, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x13, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x57, 0x61, 0x74, 0x63, 0x68, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, + 0x61, 0x72, 0x6b, 0x73, 0x22, 0x9b, 0x01, 0x0a, 0x0d, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x12, 0x35, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, + 0x72, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x70, + 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x69, 0x74, 0x65, - 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, - 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, - 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, - 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, - 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, - 0x4e, 0x10, 0x03, 0x2a, 0x55, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, - 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, - 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, - 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, - 0x42, 0x4f, 0x4f, 0x4b, 0x4d, 0x41, 0x52, 0x4b, 0x10, 0x04, 0x32, 0x8c, 0x05, 0x0a, 0x0d, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, - 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1c, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 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, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x52, 0x08, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, + 0x75, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x0e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, + 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, + 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, + 0x73, 0x68, 0x6f, 0x77, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, + 0x92, 0x01, 0x0a, 0x0f, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, + 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, + 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x8e, 0x01, 0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, + 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, + 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, + 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, + 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, + 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0xe5, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x27, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, + 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68, + 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, + 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x97, 0x01, + 0x0a, 0x0e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, + 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, + 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, + 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, + 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, + 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x2a, 0x55, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, + 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, 0x41, 0x54, + 0x45, 0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, + 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, + 0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d, 0x41, 0x52, 0x4b, 0x10, 0x04, 0x32, 0xba, 0x03, 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, 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, + 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, 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, 0x3a, 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, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x30, 0x01, 0x12, 0x3e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 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, 0x12, + 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, 0x3a, 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, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 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, + 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xce, 0x02, 0x0a, 0x0e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 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, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, + 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 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, + 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 ( @@ -2669,8 +2676,8 @@ var file_resource_proto_goTypes = []interface{}{ (*UpdateResponse)(nil), // 13: resource.UpdateResponse (*DeleteRequest)(nil), // 14: resource.DeleteRequest (*DeleteResponse)(nil), // 15: resource.DeleteResponse - (*GetResourceRequest)(nil), // 16: resource.GetResourceRequest - (*GetResourceResponse)(nil), // 17: resource.GetResourceResponse + (*ReadRequest)(nil), // 16: resource.ReadRequest + (*ReadResponse)(nil), // 17: resource.ReadResponse (*GetBlobRequest)(nil), // 18: resource.GetBlobRequest (*GetBlobResponse)(nil), // 19: resource.GetBlobResponse (*Requirement)(nil), // 20: resource.Requirement @@ -2699,8 +2706,8 @@ var file_resource_proto_depIdxs = []int32{ 8, // 7: resource.UpdateResponse.status:type_name -> resource.StatusResult 4, // 8: resource.DeleteRequest.key:type_name -> resource.ResourceKey 8, // 9: resource.DeleteResponse.status:type_name -> resource.StatusResult - 4, // 10: resource.GetResourceRequest.key:type_name -> resource.ResourceKey - 8, // 11: resource.GetResourceResponse.status:type_name -> resource.StatusResult + 4, // 10: resource.ReadRequest.key:type_name -> resource.ResourceKey + 8, // 11: resource.ReadResponse.status:type_name -> resource.StatusResult 4, // 12: resource.GetBlobRequest.key:type_name -> resource.ResourceKey 8, // 13: resource.GetBlobResponse.status:type_name -> resource.StatusResult 7, // 14: resource.GetBlobResponse.info:type_name -> resource.BlobInfo @@ -2721,28 +2728,32 @@ var file_resource_proto_depIdxs = []int32{ 4, // 29: resource.ResourceOriginInfo.key:type_name -> resource.ResourceKey 30, // 30: resource.OriginResponse.items:type_name -> resource.ResourceOriginInfo 3, // 31: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus - 16, // 32: resource.ResourceStore.GetResource:input_type -> resource.GetResourceRequest + 16, // 32: resource.ResourceStore.Read:input_type -> resource.ReadRequest 10, // 33: resource.ResourceStore.Create:input_type -> resource.CreateRequest 12, // 34: resource.ResourceStore.Update:input_type -> resource.UpdateRequest 14, // 35: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest 23, // 36: resource.ResourceStore.List:input_type -> resource.ListRequest 25, // 37: resource.ResourceStore.Watch:input_type -> resource.WatchRequest - 18, // 38: resource.ResourceStore.GetBlob:input_type -> resource.GetBlobRequest - 27, // 39: resource.ResourceStore.History:input_type -> resource.HistoryRequest - 29, // 40: resource.ResourceStore.Origin:input_type -> resource.OriginRequest - 32, // 41: resource.ResourceStore.IsHealthy:input_type -> resource.HealthCheckRequest - 17, // 42: resource.ResourceStore.GetResource:output_type -> resource.GetResourceResponse - 11, // 43: resource.ResourceStore.Create:output_type -> resource.CreateResponse - 13, // 44: resource.ResourceStore.Update:output_type -> resource.UpdateResponse - 15, // 45: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse - 24, // 46: resource.ResourceStore.List:output_type -> resource.ListResponse - 26, // 47: resource.ResourceStore.Watch:output_type -> resource.WatchResponse - 19, // 48: resource.ResourceStore.GetBlob:output_type -> resource.GetBlobResponse - 28, // 49: resource.ResourceStore.History:output_type -> resource.HistoryResponse - 31, // 50: resource.ResourceStore.Origin:output_type -> resource.OriginResponse - 33, // 51: resource.ResourceStore.IsHealthy:output_type -> resource.HealthCheckResponse - 42, // [42:52] is the sub-list for method output_type - 32, // [32:42] is the sub-list for method input_type + 32, // 38: resource.ResourceStore.IsHealthy:input_type -> resource.HealthCheckRequest + 16, // 39: resource.ResourceSearch.Read:input_type -> resource.ReadRequest + 18, // 40: resource.ResourceSearch.GetBlob:input_type -> resource.GetBlobRequest + 27, // 41: resource.ResourceSearch.History:input_type -> resource.HistoryRequest + 29, // 42: resource.ResourceSearch.Origin:input_type -> resource.OriginRequest + 32, // 43: resource.ResourceSearch.IsHealthy:input_type -> resource.HealthCheckRequest + 17, // 44: resource.ResourceStore.Read:output_type -> resource.ReadResponse + 11, // 45: resource.ResourceStore.Create:output_type -> resource.CreateResponse + 13, // 46: resource.ResourceStore.Update:output_type -> resource.UpdateResponse + 15, // 47: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse + 24, // 48: resource.ResourceStore.List:output_type -> resource.ListResponse + 26, // 49: resource.ResourceStore.Watch:output_type -> resource.WatchResponse + 33, // 50: resource.ResourceStore.IsHealthy:output_type -> resource.HealthCheckResponse + 17, // 51: resource.ResourceSearch.Read:output_type -> resource.ReadResponse + 19, // 52: resource.ResourceSearch.GetBlob:output_type -> resource.GetBlobResponse + 28, // 53: resource.ResourceSearch.History:output_type -> resource.HistoryResponse + 31, // 54: resource.ResourceSearch.Origin:output_type -> resource.OriginResponse + 33, // 55: resource.ResourceSearch.IsHealthy:output_type -> resource.HealthCheckResponse + 44, // [44:56] is the sub-list for method output_type + 32, // [32:44] is the sub-list for method input_type 32, // [32:32] is the sub-list for extension type_name 32, // [32:32] is the sub-list for extension extendee 0, // [0:32] is the sub-list for field type_name @@ -2899,7 +2910,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetResourceRequest); i { + switch v := v.(*ReadRequest); i { case 0: return &v.state case 1: @@ -2911,7 +2922,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetResourceResponse); i { + switch v := v.(*ReadResponse); i { case 0: return &v.state case 1: @@ -3123,7 +3134,7 @@ func file_resource_proto_init() { NumEnums: 4, NumMessages: 30, NumExtensions: 0, - NumServices: 1, + NumServices: 2, }, GoTypes: file_resource_proto_goTypes, DependencyIndexes: file_resource_proto_depIdxs, diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto index 3c047e12027..11a140ad5d5 100644 --- a/pkg/storage/unified/resource/resource.proto +++ b/pkg/storage/unified/resource/resource.proto @@ -186,14 +186,14 @@ message DeleteResponse { int64 resource_version = 2; } -message GetResourceRequest { +message ReadRequest { ResourceKey key = 1; // Do not include any blob details bool ignore_blob = 2; } -message GetResourceResponse { +message ReadResponse { // Status code StatusResult status = 1; @@ -408,14 +408,24 @@ message HealthCheckResponse { ServingStatus status = 1; } -// The entity store provides a basic CRUD+watch for any resource +// This provides the CRUD+List+Watch support needed for a k8s apiserver +// The semantics and behaviors of this service are constrained by kubernetes +// This does not understand the resource schemas, only deals with json bytes +// Clients should not use this interface directly; it is for use in API Servers service ResourceStore { - rpc GetResource(GetResourceRequest) returns (GetResourceResponse); + rpc Read(ReadRequest) returns (ReadResponse); rpc Create(CreateRequest) returns (CreateResponse); rpc Update(UpdateRequest) returns (UpdateResponse); rpc Delete(DeleteRequest) returns (DeleteResponse); rpc List(ListRequest) returns (ListResponse); rpc Watch(WatchRequest) returns (stream WatchResponse); + rpc IsHealthy(HealthCheckRequest) returns (HealthCheckResponse); +} + +// Unlike ResourceStore, this service does not have strict read after write guarantees +// Clients can use this service directly +service ResourceSearch { + rpc Read(ReadRequest) returns (ReadResponse); // Duplicated -- for client read only usage // Get the raw blob bytes and metadata rpc GetBlob(GetBlobRequest) returns (GetBlobResponse); diff --git a/pkg/storage/unified/resource/resource_grpc.pb.go b/pkg/storage/unified/resource/resource_grpc.pb.go index e0adfd90a4e..fccb7ccffa4 100644 --- a/pkg/storage/unified/resource/resource_grpc.pb.go +++ b/pkg/storage/unified/resource/resource_grpc.pb.go @@ -19,37 +19,30 @@ import ( const _ = grpc.SupportPackageIsVersion8 const ( - ResourceStore_GetResource_FullMethodName = "/resource.ResourceStore/GetResource" - ResourceStore_Create_FullMethodName = "/resource.ResourceStore/Create" - ResourceStore_Update_FullMethodName = "/resource.ResourceStore/Update" - ResourceStore_Delete_FullMethodName = "/resource.ResourceStore/Delete" - ResourceStore_List_FullMethodName = "/resource.ResourceStore/List" - ResourceStore_Watch_FullMethodName = "/resource.ResourceStore/Watch" - ResourceStore_GetBlob_FullMethodName = "/resource.ResourceStore/GetBlob" - ResourceStore_History_FullMethodName = "/resource.ResourceStore/History" - ResourceStore_Origin_FullMethodName = "/resource.ResourceStore/Origin" - ResourceStore_IsHealthy_FullMethodName = "/resource.ResourceStore/IsHealthy" + ResourceStore_Read_FullMethodName = "/resource.ResourceStore/Read" + ResourceStore_Create_FullMethodName = "/resource.ResourceStore/Create" + ResourceStore_Update_FullMethodName = "/resource.ResourceStore/Update" + ResourceStore_Delete_FullMethodName = "/resource.ResourceStore/Delete" + ResourceStore_List_FullMethodName = "/resource.ResourceStore/List" + ResourceStore_Watch_FullMethodName = "/resource.ResourceStore/Watch" + ResourceStore_IsHealthy_FullMethodName = "/resource.ResourceStore/IsHealthy" ) // ResourceStoreClient is the client API for ResourceStore 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. // -// The entity store provides a basic CRUD+watch for any resource +// This provides the CRUD+List+Watch support needed for a k8s apiserver +// The semantics and behaviors of this service are constrained by kubernetes +// This does not understand the resource schemas, only deals with json bytes +// Clients should not use this interface directly; it is for use in API Servers type ResourceStoreClient interface { - GetResource(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*GetResourceResponse, error) + Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateResponse, error) Update(ctx context.Context, in *UpdateRequest, opts ...grpc.CallOption) (*UpdateResponse, error) Delete(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*DeleteResponse, error) List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error) Watch(ctx context.Context, in *WatchRequest, opts ...grpc.CallOption) (ResourceStore_WatchClient, error) - // Get the raw blob bytes and metadata - GetBlob(ctx context.Context, in *GetBlobRequest, opts ...grpc.CallOption) (*GetBlobResponse, 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) - // Check if the service is healthy IsHealthy(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) } @@ -61,10 +54,10 @@ func NewResourceStoreClient(cc grpc.ClientConnInterface) ResourceStoreClient { return &resourceStoreClient{cc} } -func (c *resourceStoreClient) GetResource(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*GetResourceResponse, error) { +func (c *resourceStoreClient) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GetResourceResponse) - err := c.cc.Invoke(ctx, ResourceStore_GetResource_FullMethodName, in, out, cOpts...) + out := new(ReadResponse) + err := c.cc.Invoke(ctx, ResourceStore_Read_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -144,36 +137,6 @@ func (x *resourceStoreWatchClient) Recv() (*WatchResponse, error) { return m, nil } -func (c *resourceStoreClient) GetBlob(ctx context.Context, in *GetBlobRequest, opts ...grpc.CallOption) (*GetBlobResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GetBlobResponse) - err := c.cc.Invoke(ctx, ResourceStore_GetBlob_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *resourceStoreClient) 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, ResourceStore_History_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *resourceStoreClient) 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, ResourceStore_Origin_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *resourceStoreClient) IsHealthy(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(HealthCheckResponse) @@ -188,21 +151,17 @@ func (c *resourceStoreClient) IsHealthy(ctx context.Context, in *HealthCheckRequ // All implementations should embed UnimplementedResourceStoreServer // for forward compatibility // -// The entity store provides a basic CRUD+watch for any resource +// This provides the CRUD+List+Watch support needed for a k8s apiserver +// The semantics and behaviors of this service are constrained by kubernetes +// This does not understand the resource schemas, only deals with json bytes +// Clients should not use this interface directly; it is for use in API Servers type ResourceStoreServer interface { - GetResource(context.Context, *GetResourceRequest) (*GetResourceResponse, error) + Read(context.Context, *ReadRequest) (*ReadResponse, error) Create(context.Context, *CreateRequest) (*CreateResponse, error) Update(context.Context, *UpdateRequest) (*UpdateResponse, error) Delete(context.Context, *DeleteRequest) (*DeleteResponse, error) List(context.Context, *ListRequest) (*ListResponse, error) Watch(*WatchRequest, ResourceStore_WatchServer) error - // Get the raw blob bytes and metadata - GetBlob(context.Context, *GetBlobRequest) (*GetBlobResponse, error) - // Show resource history (and trash) - History(context.Context, *HistoryRequest) (*HistoryResponse, error) - // Used for efficient provisioning - Origin(context.Context, *OriginRequest) (*OriginResponse, error) - // Check if the service is healthy IsHealthy(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) } @@ -210,8 +169,8 @@ type ResourceStoreServer interface { type UnimplementedResourceStoreServer struct { } -func (UnimplementedResourceStoreServer) GetResource(context.Context, *GetResourceRequest) (*GetResourceResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetResource not implemented") +func (UnimplementedResourceStoreServer) Read(context.Context, *ReadRequest) (*ReadResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Read not implemented") } func (UnimplementedResourceStoreServer) Create(context.Context, *CreateRequest) (*CreateResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Create not implemented") @@ -228,15 +187,6 @@ func (UnimplementedResourceStoreServer) List(context.Context, *ListRequest) (*Li func (UnimplementedResourceStoreServer) Watch(*WatchRequest, ResourceStore_WatchServer) error { return status.Errorf(codes.Unimplemented, "method Watch not implemented") } -func (UnimplementedResourceStoreServer) GetBlob(context.Context, *GetBlobRequest) (*GetBlobResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetBlob not implemented") -} -func (UnimplementedResourceStoreServer) History(context.Context, *HistoryRequest) (*HistoryResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method History not implemented") -} -func (UnimplementedResourceStoreServer) Origin(context.Context, *OriginRequest) (*OriginResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Origin not implemented") -} func (UnimplementedResourceStoreServer) IsHealthy(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method IsHealthy not implemented") } @@ -252,20 +202,20 @@ func RegisterResourceStoreServer(s grpc.ServiceRegistrar, srv ResourceStoreServe s.RegisterService(&ResourceStore_ServiceDesc, srv) } -func _ResourceStore_GetResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetResourceRequest) +func _ResourceStore_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.(ResourceStoreServer).GetResource(ctx, in) + return srv.(ResourceStoreServer).Read(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: ResourceStore_GetResource_FullMethodName, + FullMethod: ResourceStore_Read_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ResourceStoreServer).GetResource(ctx, req.(*GetResourceRequest)) + return srv.(ResourceStoreServer).Read(ctx, req.(*ReadRequest)) } return interceptor(ctx, in, info, handler) } @@ -363,60 +313,6 @@ func (x *resourceStoreWatchServer) Send(m *WatchResponse) error { return x.ServerStream.SendMsg(m) } -func _ResourceStore_GetBlob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetBlobRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ResourceStoreServer).GetBlob(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: ResourceStore_GetBlob_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ResourceStoreServer).GetBlob(ctx, req.(*GetBlobRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ResourceStore_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.(ResourceStoreServer).History(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: ResourceStore_History_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ResourceStoreServer).History(ctx, req.(*HistoryRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ResourceStore_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.(ResourceStoreServer).Origin(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: ResourceStore_Origin_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ResourceStoreServer).Origin(ctx, req.(*OriginRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _ResourceStore_IsHealthy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(HealthCheckRequest) if err := dec(in); err != nil { @@ -443,8 +339,8 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{ HandlerType: (*ResourceStoreServer)(nil), Methods: []grpc.MethodDesc{ { - MethodName: "GetResource", - Handler: _ResourceStore_GetResource_Handler, + MethodName: "Read", + Handler: _ResourceStore_Read_Handler, }, { MethodName: "Create", @@ -462,18 +358,6 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{ MethodName: "List", Handler: _ResourceStore_List_Handler, }, - { - MethodName: "GetBlob", - Handler: _ResourceStore_GetBlob_Handler, - }, - { - MethodName: "History", - Handler: _ResourceStore_History_Handler, - }, - { - MethodName: "Origin", - Handler: _ResourceStore_Origin_Handler, - }, { MethodName: "IsHealthy", Handler: _ResourceStore_IsHealthy_Handler, @@ -488,3 +372,258 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{ }, Metadata: "resource.proto", } + +const ( + ResourceSearch_Read_FullMethodName = "/resource.ResourceSearch/Read" + ResourceSearch_GetBlob_FullMethodName = "/resource.ResourceSearch/GetBlob" + ResourceSearch_History_FullMethodName = "/resource.ResourceSearch/History" + ResourceSearch_Origin_FullMethodName = "/resource.ResourceSearch/Origin" + ResourceSearch_IsHealthy_FullMethodName = "/resource.ResourceSearch/IsHealthy" +) + +// ResourceSearchClient is the client API for ResourceSearch 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 ResourceStore, this service does not have strict read after write guarantees +// Clients can use this service directly +type ResourceSearchClient interface { + Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) + // Get the raw blob bytes and metadata + GetBlob(ctx context.Context, in *GetBlobRequest, opts ...grpc.CallOption) (*GetBlobResponse, 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) + // Check if the service is healthy + IsHealthy(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) +} + +type resourceSearchClient struct { + cc grpc.ClientConnInterface +} + +func NewResourceSearchClient(cc grpc.ClientConnInterface) ResourceSearchClient { + return &resourceSearchClient{cc} +} + +func (c *resourceSearchClient) 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, ResourceSearch_Read_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceSearchClient) GetBlob(ctx context.Context, in *GetBlobRequest, opts ...grpc.CallOption) (*GetBlobResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetBlobResponse) + err := c.cc.Invoke(ctx, ResourceSearch_GetBlob_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceSearchClient) 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, ResourceSearch_History_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceSearchClient) 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, ResourceSearch_Origin_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceSearchClient) IsHealthy(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HealthCheckResponse) + err := c.cc.Invoke(ctx, ResourceSearch_IsHealthy_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ResourceSearchServer is the server API for ResourceSearch service. +// All implementations should embed UnimplementedResourceSearchServer +// for forward compatibility +// +// Unlike ResourceStore, this service does not have strict read after write guarantees +// Clients can use this service directly +type ResourceSearchServer interface { + Read(context.Context, *ReadRequest) (*ReadResponse, error) + // Get the raw blob bytes and metadata + GetBlob(context.Context, *GetBlobRequest) (*GetBlobResponse, error) + // Show resource history (and trash) + History(context.Context, *HistoryRequest) (*HistoryResponse, error) + // Used for efficient provisioning + Origin(context.Context, *OriginRequest) (*OriginResponse, error) + // Check if the service is healthy + IsHealthy(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) +} + +// UnimplementedResourceSearchServer should be embedded to have forward compatible implementations. +type UnimplementedResourceSearchServer struct { +} + +func (UnimplementedResourceSearchServer) Read(context.Context, *ReadRequest) (*ReadResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Read not implemented") +} +func (UnimplementedResourceSearchServer) GetBlob(context.Context, *GetBlobRequest) (*GetBlobResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetBlob not implemented") +} +func (UnimplementedResourceSearchServer) History(context.Context, *HistoryRequest) (*HistoryResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method History not implemented") +} +func (UnimplementedResourceSearchServer) Origin(context.Context, *OriginRequest) (*OriginResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Origin not implemented") +} +func (UnimplementedResourceSearchServer) IsHealthy(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method IsHealthy not implemented") +} + +// UnsafeResourceSearchServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ResourceSearchServer will +// result in compilation errors. +type UnsafeResourceSearchServer interface { + mustEmbedUnimplementedResourceSearchServer() +} + +func RegisterResourceSearchServer(s grpc.ServiceRegistrar, srv ResourceSearchServer) { + s.RegisterService(&ResourceSearch_ServiceDesc, srv) +} + +func _ResourceSearch_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.(ResourceSearchServer).Read(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceSearch_Read_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceSearchServer).Read(ctx, req.(*ReadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceSearch_GetBlob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetBlobRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceSearchServer).GetBlob(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceSearch_GetBlob_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceSearchServer).GetBlob(ctx, req.(*GetBlobRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceSearch_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.(ResourceSearchServer).History(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceSearch_History_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceSearchServer).History(ctx, req.(*HistoryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceSearch_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.(ResourceSearchServer).Origin(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceSearch_Origin_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceSearchServer).Origin(ctx, req.(*OriginRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceSearch_IsHealthy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthCheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceSearchServer).IsHealthy(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceSearch_IsHealthy_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceSearchServer).IsHealthy(ctx, req.(*HealthCheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ResourceSearch_ServiceDesc is the grpc.ServiceDesc for ResourceSearch service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ResourceSearch_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "resource.ResourceSearch", + HandlerType: (*ResourceSearchServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Read", + Handler: _ResourceSearch_Read_Handler, + }, + { + MethodName: "GetBlob", + Handler: _ResourceSearch_GetBlob_Handler, + }, + { + MethodName: "History", + Handler: _ResourceSearch_History_Handler, + }, + { + MethodName: "Origin", + Handler: _ResourceSearch_Origin_Handler, + }, + { + MethodName: "IsHealthy", + Handler: _ResourceSearch_IsHealthy_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "resource.proto", +} diff --git a/pkg/storage/unified/resource/validator.go b/pkg/storage/unified/resource/validator.go deleted file mode 100644 index a8a55668c3e..00000000000 --- a/pkg/storage/unified/resource/validator.go +++ /dev/null @@ -1,256 +0,0 @@ -package resource - -import ( - "context" - "encoding/json" - "fmt" - "sync/atomic" - "time" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/bwmarrin/snowflake" - - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/apimachinery/utils" -) - -// Verify that all required fields are set, and the user has permission to set the common metadata fields -type EventValidator interface { - PrepareCreate(ctx context.Context, req *CreateRequest) (*WriteEvent, error) - PrepareUpdate(ctx context.Context, req *UpdateRequest, current *GetResourceResponse) (*WriteEvent, error) - PrepareDelete(ctx context.Context, req *DeleteRequest, current *GetResourceResponse) (*WriteEvent, error) -} - -type EventValidatorOptions struct { - // When running in a cluster, each node should have a different ID - // This is used for snowflake generation and log identification - NodeID int64 - - // Get the next EventID. When not set, this will be a snowflake ID - NextEventID func() int64 - - // Check if a user has access to write folders - // When this is nil, no resources can have folders configured - FolderAccess func(ctx context.Context, user identity.Requester, uid string) bool - - // When configured, this will make sure a user is allowed to save to a given origin - OriginAccess func(ctx context.Context, user identity.Requester, origin string) bool -} - -type eventValidator struct { - opts EventValidatorOptions -} - -func NewEventValidator(opts EventValidatorOptions) EventValidator { - if opts.NextEventID == nil { - rvGenerationNode, err := snowflake.NewNode(opts.NodeID) - if err == nil { - opts.NextEventID = func() int64 { - return rvGenerationNode.Generate().Int64() - } - } else { - counter := atomic.Int64{} - opts.NextEventID = func() int64 { - return counter.Add(1) - } - } - } - return &eventValidator{opts} -} - -type dummyObject struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` -} - -var _ EventValidator = &eventValidator{} - -func (v *eventValidator) newEvent(ctx context.Context, key *ResourceKey, value, oldValue []byte) *WriteEvent { - var err error - event := &WriteEvent{ - EventID: v.opts.NextEventID(), - Key: key, - Value: value, - } - event.Requester, err = identity.GetRequester(ctx) - if err != nil { - return event.BadRequest(err, "unable to get user") - } - - dummy := &dummyObject{} - err = json.Unmarshal(value, dummy) - if err != nil { - return event.BadRequest(err, "error reading json") - } - - obj, err := utils.MetaAccessor(dummy) - if err != nil { - return event.BadRequest(err, "invalid object in json") - } - if obj.GetUID() == "" { - return event.BadRequest(nil, "the UID must be set") - } - if obj.GetGenerateName() != "" { - return event.BadRequest(nil, "can not save value with generate name") - } - gvk := obj.GetGroupVersionKind() - if gvk.Kind == "" { - return event.BadRequest(nil, "expecting resources with a kind in the body") - } - if gvk.Version == "" { - return event.BadRequest(nil, "expecting resources with an apiVersion") - } - if gvk.Group != "" && gvk.Group != key.Group { - return event.BadRequest(nil, "group in key does not match group in the body (%s != %s)", key.Group, gvk.Group) - } - if obj.GetName() != key.Name { - return event.BadRequest(nil, "key name does not match the name in the body") - } - if obj.GetNamespace() != key.Namespace { - return event.BadRequest(nil, "key namespace does not match the namespace in the body") - } - folder := obj.GetFolder() - if folder != "" { - if v.opts.FolderAccess == nil { - return event.BadRequest(err, "folders are not supported") - } else if !v.opts.FolderAccess(ctx, event.Requester, folder) { - return event.BadRequest(err, "unable to add resource to folder") // 403? - } - } - origin, err := obj.GetOriginInfo() - if err != nil { - return event.BadRequest(err, "invalid origin info") - } - if origin != nil && v.opts.OriginAccess != nil { - if !v.opts.OriginAccess(ctx, event.Requester, origin.Name) { - return event.BadRequest(err, "not allowed to write resource to origin (%s)", origin.Name) - } - } - event.Object = obj - - // This is an update - if oldValue != nil { - dummy := &dummyObject{} - err = json.Unmarshal(oldValue, dummy) - if err != nil { - return event.BadRequest(err, "error reading old json value") - } - old, err := utils.MetaAccessor(dummy) - if err != nil { - return event.BadRequest(err, "invalid object inside old json") - } - if key.Name != old.GetName() { - return event.BadRequest(err, "the old value has a different name (%s != %s)", key.Name, old.GetName()) - } - - // Can not change creation timestamps+user - if obj.GetCreatedBy() != old.GetCreatedBy() { - return event.BadRequest(err, "can not change the created by metadata (%s != %s)", obj.GetCreatedBy(), old.GetCreatedBy()) - } - if obj.GetCreationTimestamp() != old.GetCreationTimestamp() { - return event.BadRequest(err, "can not change the CreationTimestamp metadata (%v != %v)", obj.GetCreationTimestamp(), old.GetCreationTimestamp()) - } - - oldFolder := obj.GetFolder() - if oldFolder != folder { - event.FolderChanged = true - } - event.OldObject = old - } else if folder != "" { - event.FolderChanged = true - } - return event -} - -func (v *eventValidator) PrepareCreate(ctx context.Context, req *CreateRequest) (*WriteEvent, error) { - event := v.newEvent(ctx, req.Key, req.Value, nil) - event.Operation = ResourceOperation_CREATED - if event.Status != nil { - return event, nil - } - - // Make sure the created by user is accurate - //---------------------------------------- - val := event.Object.GetCreatedBy() - if val != "" && val != event.Requester.GetUID().String() { - return event.BadRequest(nil, "created by annotation does not match: metadata.annotations#"+utils.AnnoKeyCreatedBy), nil - } - - // Create can not have updated properties - //---------------------------------------- - if event.Object.GetUpdatedBy() != "" { - return event.BadRequest(nil, "unexpected metadata.annotations#"+utils.AnnoKeyCreatedBy), nil - } - ts, err := event.Object.GetUpdatedTimestamp() - if err != nil { - return event.BadRequest(nil, fmt.Sprintf("invalid timestamp: %s", err)), nil - } - if ts != nil { - return event.BadRequest(nil, "unexpected metadata.annotations#"+utils.AnnoKeyUpdatedTimestamp), nil - } - return event, nil -} - -func (v *eventValidator) PrepareUpdate(ctx context.Context, req *UpdateRequest, current *GetResourceResponse) (*WriteEvent, error) { - event := v.newEvent(ctx, req.Key, req.Value, current.Value) - event.Operation = ResourceOperation_UPDATED - event.PreviousRV = current.ResourceVersion - if current.Value == nil { - return event.BadRequest(nil, "current value does not exist"), nil - } - if event.Status != nil { - return event, nil - } - - // Make sure the update user is accurate - //---------------------------------------- - val := event.Object.GetUpdatedBy() - if val != "" && val != event.Requester.GetUID().String() { - return event.BadRequest(nil, "updated by annotation does not match: metadata.annotations#"+utils.AnnoKeyUpdatedBy), nil - } - - return event, nil -} - -func (v *eventValidator) PrepareDelete(ctx context.Context, req *DeleteRequest, current *GetResourceResponse) (*WriteEvent, error) { - now := metav1.NewTime(time.Now()) - var err error - event := &WriteEvent{ - EventID: v.opts.NextEventID(), - Key: req.Key, - Operation: ResourceOperation_DELETED, - PreviousRV: current.ResourceVersion, - } - if event.PreviousRV != req.Key.ResourceVersion { - return event.BadRequest(err, "deletion request does not match current revision (%d != %d)", req.Key.ResourceVersion, event.PreviousRV), nil - } - event.Requester, err = identity.GetRequester(ctx) - if err != nil { - return event.BadRequest(err, "unable to get user"), nil - } - marker := &DeletedMarker{} - err = json.Unmarshal(current.Value, marker) - if err != nil { - return nil, fmt.Errorf("unable to read previous object, %w", err) - } - event.Object, err = utils.MetaAccessor(marker) - if err != nil { - return event.BadRequest(err, "unable to read marker object"), nil - } - event.Object.SetDeletionTimestamp(&now) - event.Object.SetUpdatedTimestamp(&now.Time) - event.Object.SetManagedFields(nil) - event.Object.SetFinalizers(nil) - event.Object.SetUpdatedBy(event.Requester.GetUID().String()) - marker.TypeMeta = metav1.TypeMeta{ - Kind: "DeletedMarker", - APIVersion: "storage.grafana.app/v0alpha1", // ?? or can we stick this in common? - } - marker.Annotations["RestoreResourceVersion"] = fmt.Sprintf("%d", event.PreviousRV) - event.Value, err = json.Marshal(marker) - if err != nil { - return nil, fmt.Errorf("unable creating deletion marker, %w", err) - } - return event, nil -} diff --git a/pkg/storage/unified/resource/writer.go b/pkg/storage/unified/resource/writer.go new file mode 100644 index 00000000000..9f4792d2677 --- /dev/null +++ b/pkg/storage/unified/resource/writer.go @@ -0,0 +1,330 @@ +package resource + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/bwmarrin/snowflake" + "github.com/prometheus/client_golang/prometheus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +// Package-level errors. +var ( + ErrNotFound = errors.New("entity not found") + ErrOptimisticLockingFailed = errors.New("optimistic locking failed") + ErrUserNotFoundInContext = errors.New("user not found in context") + ErrUnableToReadResourceJSON = errors.New("unable to read resource json") + ErrNextPageTokenNotSupported = errors.New("nextPageToken not yet supported") + ErrLimitNotSupported = errors.New("limit not yet supported") + ErrNotImplementedYet = errors.New("not implemented yet") +) + +// Resource writer support +type ResourceWriter interface { + Create(context.Context, *CreateRequest) (*CreateResponse, error) + Update(context.Context, *UpdateRequest) (*UpdateResponse, error) + Delete(context.Context, *DeleteRequest) (*DeleteResponse, error) +} + +type WriterOptions struct { + // OTel tracer + Tracer trace.Tracer + + // When running in a cluster, each node should have a different ID + // This is used for snowflake generation and log identification + NodeID int64 + + // Read an individual item + Reader func(context.Context, *ReadRequest) (*ReadResponse, error) + + // Add a validated write event + Appender EventAppender + + // Get the next EventID. When not set, this will default to snowflake IDs + NextEventID func() int64 + + // Check if a user has access to write folders + // When this is nil, no resources can have folders configured + FolderAccess func(ctx context.Context, user identity.Requester, uid string) bool + + // When configured, this will make sure a user is allowed to save to a given origin + OriginAccess func(ctx context.Context, user identity.Requester, origin string) bool +} + +func NewResourceWriter(opts WriterOptions) (ResourceWriter, error) { + log := slog.Default().With("logger", "resource-writer") + if err := prometheus.Register(NewStorageMetrics()); err != nil { + log.Warn("error registering storage server metrics", "error", err) + } + if opts.NextEventID == nil { + eventNode, err := snowflake.NewNode(opts.NodeID) + if err != nil { + return nil, fmt.Errorf("error initializing snowflake id generator :: %w", err) + } + opts.NextEventID = func() int64 { + return eventNode.Generate().Int64() + } + } + return &writeServer{ + log: log, + opts: opts, + }, nil +} + +// Internal object with just Type+ObjectMeta +type dummyObject struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` +} + +type writeServer struct { + log *slog.Logger + opts WriterOptions +} + +func (s *writeServer) newEvent(ctx context.Context, key *ResourceKey, value, oldValue []byte) (*WriteEvent, error) { + var err error + event := &WriteEvent{ + EventID: s.opts.NextEventID(), + Key: key, + Value: value, + } + event.Requester, err = identity.GetRequester(ctx) + if err != nil { + return nil, ErrUserNotFoundInContext + } + + dummy := &dummyObject{} + err = json.Unmarshal(value, dummy) + if err != nil { + return nil, ErrUnableToReadResourceJSON + } + + obj, err := utils.MetaAccessor(dummy) + if err != nil { + return nil, fmt.Errorf("invalid object in json") + } + if obj.GetUID() == "" { + return nil, fmt.Errorf("the UID must be set") + } + if obj.GetGenerateName() != "" { + return nil, fmt.Errorf("can not save value with generate name") + } + gvk := obj.GetGroupVersionKind() + if gvk.Kind == "" { + return nil, fmt.Errorf("expecting resources with a kind in the body") + } + if gvk.Version == "" { + return nil, fmt.Errorf("expecting resources with an apiVersion") + } + if gvk.Group != "" && gvk.Group != key.Group { + return nil, fmt.Errorf("group in key does not match group in the body (%s != %s)", key.Group, gvk.Group) + } + if obj.GetName() != key.Name { + return nil, fmt.Errorf("key name does not match the name in the body") + } + if obj.GetNamespace() != key.Namespace { + return nil, fmt.Errorf("key namespace does not match the namespace in the body") + } + folder := obj.GetFolder() + if folder != "" { + if s.opts.FolderAccess == nil { + return nil, fmt.Errorf("folders are not supported") + } else if !s.opts.FolderAccess(ctx, event.Requester, folder) { + return nil, fmt.Errorf("unable to add resource to folder") // 403? + } + } + origin, err := obj.GetOriginInfo() + if err != nil { + return nil, fmt.Errorf("invalid origin info") + } + if origin != nil && s.opts.OriginAccess != nil { + if !s.opts.OriginAccess(ctx, event.Requester, origin.Name) { + return nil, fmt.Errorf("not allowed to write resource to origin (%s)", origin.Name) + } + } + event.Object = obj + + // This is an update + if oldValue != nil { + dummy := &dummyObject{} + err = json.Unmarshal(oldValue, dummy) + if err != nil { + return nil, fmt.Errorf("error reading old json value") + } + old, err := utils.MetaAccessor(dummy) + if err != nil { + return nil, fmt.Errorf("invalid object inside old json") + } + if key.Name != old.GetName() { + return nil, fmt.Errorf("the old value has a different name (%s != %s)", key.Name, old.GetName()) + } + + // Can not change creation timestamps+user + if obj.GetCreatedBy() != old.GetCreatedBy() { + return nil, fmt.Errorf("can not change the created by metadata (%s != %s)", obj.GetCreatedBy(), old.GetCreatedBy()) + } + if obj.GetCreationTimestamp() != old.GetCreationTimestamp() { + return nil, fmt.Errorf("can not change the CreationTimestamp metadata (%v != %v)", obj.GetCreationTimestamp(), old.GetCreationTimestamp()) + } + + oldFolder := obj.GetFolder() + if oldFolder != folder { + event.FolderChanged = true + } + event.OldObject = old + } else if folder != "" { + event.FolderChanged = true + } + return event, nil +} + +func (s *writeServer) Create(ctx context.Context, req *CreateRequest) (*CreateResponse, error) { + ctx, span := s.opts.Tracer.Start(ctx, "storage_server.Create") + defer span.End() + + if req.Key.ResourceVersion > 0 { + return nil, fmt.Errorf("can not update a specific resource version") + } + + event, err := s.newEvent(ctx, req.Key, req.Value, nil) + if err != nil { + return nil, err + } + event.Operation = ResourceOperation_CREATED + event.Blob = req.Blob + + rsp := &CreateResponse{} + // Make sure the created by user is accurate + //---------------------------------------- + val := event.Object.GetCreatedBy() + if val != "" && val != event.Requester.GetUID().String() { + return nil, fmt.Errorf("created by annotation does not match: metadata.annotations#" + utils.AnnoKeyCreatedBy) + } + + // Create can not have updated properties + //---------------------------------------- + if event.Object.GetUpdatedBy() != "" { + return nil, fmt.Errorf("unexpected metadata.annotations#" + utils.AnnoKeyCreatedBy) + } + + ts, err := event.Object.GetUpdatedTimestamp() + if err != nil { + return nil, fmt.Errorf(fmt.Sprintf("invalid timestamp: %s", err)) + } + if ts != nil { + return nil, fmt.Errorf("unexpected metadata.annotations#" + utils.AnnoKeyUpdatedTimestamp) + } + + // Append and set the resource version + rsp.ResourceVersion, err = s.opts.Appender(ctx, event) + // ?? convert the error to status? + return rsp, err +} + +func (s *writeServer) Update(ctx context.Context, req *UpdateRequest) (*UpdateResponse, error) { + ctx, span := s.opts.Tracer.Start(ctx, "storage_server.Update") + defer span.End() + + rsp := &UpdateResponse{} + if req.Key.ResourceVersion < 0 { + return nil, fmt.Errorf("update must include the previous version") + } + + latest, err := s.opts.Reader(ctx, &ReadRequest{ + Key: req.Key.WithoutResourceVersion(), + }) + if err != nil { + return nil, err + } + if latest.Value == nil { + return nil, fmt.Errorf("current value does not exist") + } + + event, err := s.newEvent(ctx, req.Key, req.Value, latest.Value) + if err != nil { + return nil, err + } + event.Operation = ResourceOperation_UPDATED + event.PreviousRV = latest.ResourceVersion + + // Make sure the update user is accurate + //---------------------------------------- + val := event.Object.GetUpdatedBy() + if val != "" && val != event.Requester.GetUID().String() { + return nil, fmt.Errorf("updated by annotation does not match: metadata.annotations#" + utils.AnnoKeyUpdatedBy) + } + + rsp.ResourceVersion, err = s.opts.Appender(ctx, event) + + return rsp, err +} + +func (s *writeServer) Delete(ctx context.Context, req *DeleteRequest) (*DeleteResponse, error) { + ctx, span := s.opts.Tracer.Start(ctx, "storage_server.Delete") + defer span.End() + + rsp := &DeleteResponse{} + if req.Key.ResourceVersion < 0 { + return nil, fmt.Errorf("update must include the previous version") + } + + latest, err := s.opts.Reader(ctx, &ReadRequest{ + Key: req.Key.WithoutResourceVersion(), + }) + if err != nil { + return nil, err + } + if latest.ResourceVersion != req.Key.ResourceVersion { + return nil, fmt.Errorf("deletion request does not match current revision (%d != %d)", req.Key.ResourceVersion, latest.ResourceVersion) + } + + now := metav1.NewTime(time.Now()) + event := &WriteEvent{ + EventID: s.opts.NextEventID(), + Key: req.Key, + Operation: ResourceOperation_DELETED, + PreviousRV: latest.ResourceVersion, + } + event.Requester, err = identity.GetRequester(ctx) + if err != nil { + return nil, fmt.Errorf("unable to get user") + } + marker := &DeletedMarker{} + err = json.Unmarshal(latest.Value, marker) + if err != nil { + return nil, fmt.Errorf("unable to read previous object, %w", err) + } + event.Object, err = utils.MetaAccessor(marker) + if err != nil { + return nil, err + } + event.Object.SetDeletionTimestamp(&now) + event.Object.SetUpdatedTimestamp(&now.Time) + event.Object.SetManagedFields(nil) + event.Object.SetFinalizers(nil) + event.Object.SetUpdatedBy(event.Requester.GetUID().String()) + marker.TypeMeta = metav1.TypeMeta{ + Kind: "DeletedMarker", + APIVersion: "storage.grafana.app/v0alpha1", // ?? or can we stick this in common? + } + marker.Annotations["RestoreResourceVersion"] = fmt.Sprintf("%d", event.PreviousRV) + event.Value, err = json.Marshal(marker) + if err != nil { + return nil, fmt.Errorf("unable creating deletion marker, %w", err) + } + + rsp.ResourceVersion, err = s.opts.Appender(ctx, event) + + return rsp, err +} diff --git a/pkg/storage/unified/resource/writer_test.go b/pkg/storage/unified/resource/writer_test.go new file mode 100644 index 00000000000..64daaac71b9 --- /dev/null +++ b/pkg/storage/unified/resource/writer_test.go @@ -0,0 +1,103 @@ +package resource + +import ( + "context" + "embed" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/services/user" +) + +func TestWriter(t *testing.T) { + tracer := noop.NewTracerProvider().Tracer("testing") + testUserA := &user.SignedInUser{ + UserID: 123, + UserUID: "u123", + OrgRole: identity.RoleAdmin, + } + ctx := appcontext.WithUser(context.Background(), testUserA) + + store := NewMemoryStore() + writer, err := NewResourceWriter(WriterOptions{ + Tracer: tracer, + Reader: store.Read, + Appender: store.WriteEvent, + }) + require.NoError(t, err) + + t.Run("playlist happy CRUD paths", func(t *testing.T) { + raw := testdata(t, "01_create_playlist.json") + key := &ResourceKey{ + Group: "playlist.grafana.app", + Resource: "rrrr", // can be anything :( + Namespace: "default", + Name: "fdgsv37qslr0ga", + } + created, err := writer.Create(ctx, &CreateRequest{ + Value: raw, + Key: key, + }) + require.NoError(t, err) + require.True(t, created.ResourceVersion > 0) + + // The key does not include resource version + found, err := store.Read(ctx, &ReadRequest{Key: key}) + require.NoError(t, err) + require.Equal(t, created.ResourceVersion, found.ResourceVersion) + + // Now update the value + tmp := &unstructured.Unstructured{} + err = json.Unmarshal(raw, tmp) + require.NoError(t, err) + + now := time.Now().UnixMilli() + obj, err := utils.MetaAccessor(tmp) + require.NoError(t, err) + obj.SetAnnotation("test", "hello") + obj.SetUpdatedTimestampMillis(now) + obj.SetUpdatedBy(testUserA.GetUID().String()) + raw, err = json.Marshal(tmp) + require.NoError(t, err) + + key.ResourceVersion = created.ResourceVersion + updated, err := writer.Update(ctx, &UpdateRequest{Key: key, Value: raw}) + require.NoError(t, err) + require.True(t, updated.ResourceVersion > created.ResourceVersion) + + // We should still get the latest + key.ResourceVersion = 0 + found, err = store.Read(ctx, &ReadRequest{Key: key}) + require.NoError(t, err) + require.Equal(t, updated.ResourceVersion, found.ResourceVersion) + + key.ResourceVersion = updated.ResourceVersion + deleted, err := writer.Delete(ctx, &DeleteRequest{Key: key}) + require.NoError(t, err) + require.True(t, deleted.ResourceVersion > updated.ResourceVersion) + + // We should get not found when trying to read the latest value + key.ResourceVersion = 0 + found, _ = store.Read(ctx, &ReadRequest{Key: key}) + require.Equal(t, int32(404), found.Status.Code) + require.Nil(t, found.Value) + }) +} + +//go:embed testdata/* +var testdataFS embed.FS + +func testdata(t *testing.T, filename string) []byte { + t.Helper() + b, err := testdataFS.ReadFile(`testdata/` + filename) + require.NoError(t, err) + return b +} diff --git a/pkg/storage/unified/sqlstash/queries_test.go b/pkg/storage/unified/sqlstash/queries_test.go deleted file mode 100644 index 2a5ce2c22e4..00000000000 --- a/pkg/storage/unified/sqlstash/queries_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package sqlstash - -import ( - "context" - "embed" - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/apimachinery/identity" - playlist "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/storage/unified/resource" -) - -func TestSQLCommands(t *testing.T) { - ctx := appcontext.WithUser(context.Background(), &user.SignedInUser{ - UserID: 123, - UserUID: "u123", - OrgRole: identity.RoleAdmin, - }) - validator := resource.NewEventValidator(resource.EventValidatorOptions{ - // no folders for now - }) - - t.Run("insert playlist SQL", func(t *testing.T) { - input := testdataFromJSON(t, "01_create_playlist.json", &playlist.Playlist{}) - key, err := resource.ResourceKeyFor(playlist.PlaylistResourceInfo.GroupResource(), input) - require.NoError(t, err) - - req := &resource.CreateRequest{Key: key, Message: "test commit"} - req.Value, err = json.Marshal(input) - require.NoError(t, err) - require.Equal(t, "default/playlist.grafana.app/playlists/fdgsv37qslr0ga", key.NamespacedPath()) - - evt, err := validator.PrepareCreate(ctx, req) - require.NoError(t, err) - require.NoError(t, evt.Error) - require.Nil(t, evt.Error) - }) -} - -//go:embed testdata/* -var testdataFS embed.FS - -func testdata(t *testing.T, filename string) []byte { - t.Helper() - b, err := testdataFS.ReadFile(`testdata/` + filename) - require.NoError(t, err) - - return b -} - -func testdataFromJSON[T any](t *testing.T, filename string, dest T) T { - t.Helper() - b := testdata(t, filename) - err := json.Unmarshal(b, dest) - require.NoError(t, err) - return dest -} diff --git a/pkg/storage/unified/sqlstash/sql_storage_server.go b/pkg/storage/unified/sqlstash/sql_storage_server.go index f4c93d42bf7..a9b187d3bbe 100644 --- a/pkg/storage/unified/sqlstash/sql_storage_server.go +++ b/pkg/storage/unified/sqlstash/sql_storage_server.go @@ -21,9 +21,6 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" ) -const resoruceTable = "resource" -const resourceVersionTable = "resource_version" - // Package-level errors. var ( ErrNotFound = errors.New("entity not found") @@ -34,8 +31,9 @@ var ( ErrNotImplementedYet = errors.New("not implemented yet") ) -// Make sure we implement correct interfaces +// Make sure we implement both store and search var _ resource.ResourceStoreServer = &sqlResourceServer{} +var _ resource.ResourceSearchServer = &sqlResourceServer{} func ProvideSQLResourceServer(db db.EntityDBInterface, tracer tracing.Tracer) (SqlResourceServer, error) { ctx, cancel := context.WithCancel(context.Background()) @@ -72,7 +70,7 @@ type sqlResourceServer struct { cancel context.CancelFunc stream chan *resource.WatchResponse tracer trace.Tracer - validator resource.EventValidator + writer resource.ResourceWriter once sync.Once initErr error @@ -138,9 +136,15 @@ func (s *sqlResourceServer) init() error { s.sess = sess s.dialect = migrator.NewDialect(engine.DriverName()) - s.validator = resource.NewEventValidator(resource.EventValidatorOptions{ - // use snowflake IDs + s.writer, err = resource.NewResourceWriter(resource.WriterOptions{ + NodeID: 10, + Tracer: s.tracer, + Reader: s.Read, + Appender: s.append, }) + if err != nil { + return err + } // set up the broadcaster s.broadcaster, err = sqlstash.NewBroadcaster(s.ctx, func(stream chan *resource.WatchResponse) error { @@ -176,8 +180,15 @@ func (s *sqlResourceServer) Stop() { s.cancel() } -func (s *sqlResourceServer) GetResource(ctx context.Context, req *resource.GetResourceRequest) (*resource.GetResourceResponse, error) { - ctx, span := s.tracer.Start(ctx, "storage_server.GetResource") +func (s *sqlResourceServer) append(ctx context.Context, event *resource.WriteEvent) (int64, error) { + _, span := s.tracer.Start(ctx, "storage_server.WriteEvent") + defer span.End() + + return 0, ErrNotImplementedYet +} + +func (s *sqlResourceServer) Read(ctx context.Context, req *resource.ReadRequest) (*resource.ReadResponse, error) { + _, span := s.tracer.Start(ctx, "storage_server.GetResource") defer span.End() if err := s.Init(); err != nil { @@ -185,10 +196,10 @@ func (s *sqlResourceServer) GetResource(ctx context.Context, req *resource.GetRe } if req.Key.Group == "" { - return &resource.GetResourceResponse{Status: badRequest("missing group")}, nil + return &resource.ReadResponse{Status: badRequest("missing group")}, nil } if req.Key.Resource == "" { - return &resource.GetResourceResponse{Status: badRequest("missing resource")}, nil + return &resource.ReadResponse{Status: badRequest("missing resource")}, nil } fmt.Printf("TODO, GET: %+v", req.Key) @@ -197,96 +208,39 @@ func (s *sqlResourceServer) GetResource(ctx context.Context, req *resource.GetRe } func (s *sqlResourceServer) Create(ctx context.Context, req *resource.CreateRequest) (*resource.CreateResponse, error) { - ctx, span := s.tracer.Start(ctx, "storage_server.Create") - defer span.End() - - if req.Key.ResourceVersion > 0 { - return &resource.CreateResponse{ - Status: badRequest("can not update a specific resource version"), - }, nil - } - - if err := s.Init(); err != nil { - return nil, err - } - - event, err := s.validator.PrepareCreate(ctx, req) + rsp, err := s.writer.Create(ctx, req) if err != nil { - return nil, err + s.log.Info("create", "error", err) + rsp.Status = &resource.StatusResult{ + Status: "Failure", + Message: err.Error(), + } } - if event.Status != nil { - return &resource.CreateResponse{Status: event.Status}, nil - } - - fmt.Printf("TODO, CREATE: %v", event) - - return nil, ErrNotImplementedYet + return rsp, nil } func (s *sqlResourceServer) Update(ctx context.Context, req *resource.UpdateRequest) (*resource.UpdateResponse, error) { - ctx, span := s.tracer.Start(ctx, "storage_server.Update") - defer span.End() - - if req.Key.ResourceVersion < 0 { - return &resource.UpdateResponse{ - Status: badRequest("update must include the previous version"), - }, nil - } - - if err := s.Init(); err != nil { - return nil, err - } - - latest, err := s.GetResource(ctx, &resource.GetResourceRequest{ - Key: req.Key.WithoutResourceVersion(), - }) + rsp, err := s.writer.Update(ctx, req) if err != nil { - return nil, err + s.log.Info("create", "error", err) + rsp.Status = &resource.StatusResult{ + Status: "Failure", + Message: err.Error(), + } } - event, err := s.validator.PrepareUpdate(ctx, req, latest) - if err != nil { - return nil, err - } - if event.Status != nil { - return &resource.UpdateResponse{Status: event.Status}, nil - } - - fmt.Printf("TODO, UPDATE: %v", event) - - return nil, ErrNotImplementedYet + return rsp, nil } func (s *sqlResourceServer) Delete(ctx context.Context, req *resource.DeleteRequest) (*resource.DeleteResponse, error) { - ctx, span := s.tracer.Start(ctx, "storage_server.Delete") - defer span.End() - - if req.Key.ResourceVersion < 0 { - return &resource.DeleteResponse{ - Status: badRequest("update must include the previous version"), - }, nil - } - - if err := s.Init(); err != nil { - return nil, err - } - - latest, err := s.GetResource(ctx, &resource.GetResourceRequest{ - Key: req.Key.WithoutResourceVersion(), - }) + rsp, err := s.writer.Delete(ctx, req) if err != nil { - return nil, err + s.log.Info("create", "error", err) + rsp.Status = &resource.StatusResult{ + Status: "Failure", + Message: err.Error(), + } } - event, err := s.validator.PrepareDelete(ctx, req, latest) - if err != nil { - return nil, err - } - if event.Status != nil { - return &resource.DeleteResponse{Status: event.Status}, nil - } - - fmt.Printf("TODO, DELETE: %+v ", req.Key) - - return nil, ErrNotImplementedYet + return rsp, nil } func (s *sqlResourceServer) List(ctx context.Context, req *resource.ListRequest) (*resource.ListResponse, error) {