diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9e0a8720582..aea94e92eeb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -298,6 +298,7 @@ /pkg/modules/ @grafana/grafana-app-platform-squad /pkg/services/grpcserver/ @grafana/grafana-search-and-storage /pkg/generated @grafana/grafana-app-platform-squad +/pkg/services/unifiedSearch/ @grafana/grafana-search-and-storage # Alerting /pkg/services/ngalert/ @grafana/alerting-backend diff --git a/pkg/api/api.go b/pkg/api/api.go index a774627ba5d..e79526134cc 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -306,6 +306,10 @@ func (hs *HTTPServer) registerRoutes() { apiRoute.Group("/search-v2", hs.SearchV2HTTPService.RegisterHTTPRoutes) } + if hs.Features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearch) { + apiRoute.Group("/unified-search", hs.UnifiedSearchHTTPService.RegisterHTTPRoutes) + } + // current org apiRoute.Group("/org", func(orgRoute routing.RouteRegister) { userIDScope := ac.Scope("users", "id", ac.Parameter(":userId")) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index f9908deaadf..06ce9cc4b17 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -104,6 +104,7 @@ import ( "github.com/grafana/grafana/pkg/services/tag" "github.com/grafana/grafana/pkg/services/team" tempUser "github.com/grafana/grafana/pkg/services/temp_user" + "github.com/grafana/grafana/pkg/services/unifiedSearch" "github.com/grafana/grafana/pkg/services/updatechecker" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/validations" @@ -156,6 +157,7 @@ type HTTPServer struct { LivePushGateway *pushhttp.Gateway StorageService store.StorageService SearchV2HTTPService searchV2.SearchHTTPService + UnifiedSearchHTTPService unifiedSearch.SearchHTTPService ContextHandler *contexthandler.ContextHandler LoggerMiddleware loggermw.Logger SQLStore db.DB @@ -266,7 +268,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service, loginAttemptService loginAttempt.Service, orgService org.Service, teamService team.Service, accesscontrolService accesscontrol.Service, navTreeService navtree.Service, - annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService, + annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, unifiedSearchHTTPService unifiedSearch.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService, statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service, promGatherer prometheus.Gatherer, starApi *starApi.API, promRegister prometheus.Registerer, clientConfigProvider grafanaapiserver.DirectRestConfigProvider, anonService anonymous.Service, userVerifier user.Verifier, @@ -308,6 +310,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi AccessControl: accessControl, DataProxy: dataSourceProxy, SearchV2HTTPService: searchv2HTTPService, + UnifiedSearchHTTPService: unifiedSearchHTTPService, SearchService: searchService, Live: live, LivePushGateway: livePushGateway, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 5549a7d5966..1d31c7b1d35 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -149,6 +149,7 @@ import ( "github.com/grafana/grafana/pkg/services/team/teamimpl" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/services/temp_user/tempuserimpl" + "github.com/grafana/grafana/pkg/services/unifiedSearch" "github.com/grafana/grafana/pkg/services/updatechecker" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" @@ -229,6 +230,8 @@ var wireBasicSet = wire.NewSet( search.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, + unifiedSearch.ProvideService, + unifiedSearch.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, diff --git a/pkg/services/pluginsintegration/plugins_integration_test.go b/pkg/services/pluginsintegration/plugins_integration_test.go index 66d6dda25d5..428fa1b8364 100644 --- a/pkg/services/pluginsintegration/plugins_integration_test.go +++ b/pkg/services/pluginsintegration/plugins_integration_test.go @@ -91,7 +91,7 @@ func TestIntegrationPluginManager(t *testing.T) { ms := mssql.ProvideService(cfg) db := db.InitTestDB(t, sqlstore.InitTestDBOpt{Cfg: cfg}) sv2 := searchV2.ProvideService(cfg, db, nil, nil, tracer, features, nil, nil, nil) - graf := grafanads.ProvideService(sv2, nil) + graf := grafanads.ProvideService(sv2, nil, nil, features) pyroscope := pyroscope.ProvideService(hcp) parca := parca.ProvideService(hcp) coreRegistry := coreplugin.ProvideCoreRegistry(tracing.InitializeTracerForTest(), am, cw, cm, es, grap, idb, lk, otsdb, pr, tmpo, td, pg, my, ms, graf, pyroscope, parca) diff --git a/pkg/services/unifiedSearch/http.go b/pkg/services/unifiedSearch/http.go new file mode 100644 index 00000000000..ed885b1c40f --- /dev/null +++ b/pkg/services/unifiedSearch/http.go @@ -0,0 +1,73 @@ +package unifiedSearch + +import ( + "encoding/json" + "errors" + "io" + "net/http" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/middleware" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" +) + +type SearchHTTPService interface { + RegisterHTTPRoutes(storageRoute routing.RouteRegister) +} + +type searchHTTPService struct { + search SearchService +} + +func ProvideSearchHTTPService(search SearchService) SearchHTTPService { + return &searchHTTPService{search: search} +} + +func (s *searchHTTPService) RegisterHTTPRoutes(storageRoute routing.RouteRegister) { + storageRoute.Post("/", middleware.ReqSignedIn, routing.Wrap(s.doQuery)) +} + +func (s *searchHTTPService) doQuery(c *contextmodel.ReqContext) response.Response { + searchReadinessCheckResp := s.search.IsReady(c.Req.Context(), c.SignedInUser.GetOrgID()) + if !searchReadinessCheckResp.IsReady { + return response.JSON(http.StatusOK, &backend.DataResponse{ + Frames: []*data.Frame{{ + Name: "Loading", + }}, + Error: nil, + }) + } + + body, err := io.ReadAll(c.Req.Body) + if err != nil { + return response.Error(http.StatusInternalServerError, "error reading bytes", err) + } + + query := &Query{} + err = json.Unmarshal(body, query) + if err != nil { + return response.Error(http.StatusBadRequest, "error parsing body", err) + } + + resp := s.search.doQuery(c.Req.Context(), c.SignedInUser, c.SignedInUser.GetOrgID(), *query) + + if resp.Error != nil { + return response.Error(http.StatusInternalServerError, "error handling search request", resp.Error) + } + + if len(resp.Frames) == 0 { + msg := "invalid search response" + return response.Error(http.StatusInternalServerError, msg, errors.New(msg)) + } + + bytes, err := resp.MarshalJSON() + if err != nil { + return response.Error(http.StatusInternalServerError, "error marshalling response", err) + } + + return response.JSON(http.StatusOK, bytes) +} diff --git a/pkg/services/unifiedSearch/service.go b/pkg/services/unifiedSearch/service.go new file mode 100644 index 00000000000..eb83ac8bbaa --- /dev/null +++ b/pkg/services/unifiedSearch/service.go @@ -0,0 +1,176 @@ +package unifiedSearch + +import ( + "context" + "errors" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +type StandardSearchService struct { + registry.BackgroundService + cfg *setting.Cfg + sql db.DB + ac accesscontrol.Service + orgService org.Service + userService user.Service + logger log.Logger + reIndexCh chan struct{} + features featuremgmt.FeatureToggles + resourceClient resource.ResourceClient +} + +func (s *StandardSearchService) IsReady(ctx context.Context, orgId int64) IsSearchReadyResponse { + return IsSearchReadyResponse{IsReady: true} +} + +func ProvideService(cfg *setting.Cfg, sql db.DB, entityEventStore store.EntityEventsService, + ac accesscontrol.Service, tracer tracing.Tracer, features featuremgmt.FeatureToggles, orgService org.Service, + userService user.Service, folderStore folder.Store, resourceClient resource.ResourceClient) SearchService { + logger := log.New("searchV3") + s := &StandardSearchService{ + cfg: cfg, + sql: sql, + ac: ac, + logger: logger, + reIndexCh: make(chan struct{}, 1), + orgService: orgService, + userService: userService, + features: features, + resourceClient: resourceClient, + } + return s +} + +func (s *StandardSearchService) IsDisabled() bool { + return !s.features.IsEnabledGlobally(featuremgmt.FlagPanelTitleSearch) +} + +func (s *StandardSearchService) Run(ctx context.Context) error { + // TODO: implement this? ( copied from pkg/services/searchV2/service.go ) + // orgQuery := &org.SearchOrgsQuery{} + // result, err := s.orgService.Search(ctx, orgQuery) + // if err != nil { + // return fmt.Errorf("can't get org list: %w", err) + // } + // orgIDs := make([]int64, 0, len(result)) + // for _, org := range result { + // orgIDs = append(orgIDs, org.ID) + // } + // TODO: do we need to initialize the bleve index again ( should be initialized on startup )? + // return s.dashboardIndex.run(ctx, orgIDs, s.reIndexCh) + return nil +} + +func (s *StandardSearchService) TriggerReIndex() { + select { + case s.reIndexCh <- struct{}{}: + default: + // channel is full => re-index will happen soon anyway. + } +} + +func (s *StandardSearchService) getUser(ctx context.Context, backendUser *backend.User, orgId int64) (*user.SignedInUser, error) { + // TODO: get user & user's permissions from the request context + var usr *user.SignedInUser + if s.cfg.AnonymousEnabled && backendUser.Email == "" && backendUser.Login == "" { + getOrg := org.GetOrgByNameQuery{Name: s.cfg.AnonymousOrgName} + orga, err := s.orgService.GetByName(ctx, &getOrg) + if err != nil { + s.logger.Error("Anonymous access organization error.", "org_name", s.cfg.AnonymousOrgName, "error", err) + return nil, err + } + + usr = &user.SignedInUser{ + OrgID: orga.ID, + OrgName: orga.Name, + OrgRole: org.RoleType(s.cfg.AnonymousOrgRole), + IsAnonymous: true, + } + } else { + getSignedInUserQuery := &user.GetSignedInUserQuery{ + Login: backendUser.Login, + Email: backendUser.Email, + OrgID: orgId, + } + var err error + usr, err = s.userService.GetSignedInUser(ctx, getSignedInUserQuery) + if err != nil { + s.logger.Error("Error while retrieving user", "error", err, "email", backendUser.Email, "login", getSignedInUserQuery.Login) + return nil, errors.New("auth error") + } + + if usr == nil { + s.logger.Error("No user found", "email", backendUser.Email) + return nil, errors.New("auth error") + } + } + + if usr.Permissions == nil { + usr.Permissions = make(map[int64]map[string][]string) + } + + if _, ok := usr.Permissions[orgId]; ok { + // permissions as part of the `s.sql.GetSignedInUser` query - return early + return usr, nil + } + + // TODO: ensure this is cached + permissions, err := s.ac.GetUserPermissions(ctx, usr, + accesscontrol.Options{ReloadCache: false}) + if err != nil { + s.logger.Error("Failed to retrieve user permissions", "error", err, "email", backendUser.Email) + return nil, errors.New("auth error") + } + + usr.Permissions[orgId] = accesscontrol.GroupScopesByActionContext(ctx, permissions) + return usr, nil +} + +func (s *StandardSearchService) DoQuery(ctx context.Context, user *backend.User, orgID int64, q Query) *backend.DataResponse { + signedInUser, err := s.getUser(ctx, user, orgID) + if err != nil { + return &backend.DataResponse{Error: err} + } + + query := s.doQuery(ctx, signedInUser, orgID, q) + return query +} + +func (s *StandardSearchService) doQuery(ctx context.Context, signedInUser *user.SignedInUser, orgID int64, q Query) *backend.DataResponse { + response := s.doSearchQuery(ctx, q, s.cfg.AppSubURL) + return response +} + +func (s *StandardSearchService) doSearchQuery(ctx context.Context, qry Query, _ string) *backend.DataResponse { + response := &backend.DataResponse{} + + req := &resource.SearchRequest{Tenant: s.cfg.StackID, Query: qry.Query} + res, err := s.resourceClient.Search(ctx, req) + if err != nil { + response.Error = err + return response + } + + // TODO: implement this correctly + frame := data.NewFrame("results", data.NewField("value", nil, []string{})) + frame.Meta = &data.FrameMeta{Notices: []data.Notice{{Text: "TODO"}}} + for _, r := range res.Items { + frame.AppendRow(string(r.Value)) + } + response.Frames = append(response.Frames, frame) + return response +} diff --git a/pkg/services/unifiedSearch/types.go b/pkg/services/unifiedSearch/types.go new file mode 100644 index 00000000000..8faa56f93f0 --- /dev/null +++ b/pkg/services/unifiedSearch/types.go @@ -0,0 +1,49 @@ +package unifiedSearch + +import ( + "context" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/user" +) + +type FacetField struct { + Field string `json:"field"` + Limit int `json:"limit,omitempty"` // explicit page size +} + +type Query struct { + Query string `json:"query"` + Location string `json:"location,omitempty"` // parent folder ID + Sort string `json:"sort,omitempty"` // field ASC/DESC + Datasource string `json:"ds_uid,omitempty"` // "datasource" collides with the JSON value at the same level :() + DatasourceType string `json:"ds_type,omitempty"` + Tags []string `json:"tags,omitempty"` + Kind []string `json:"kind,omitempty"` + PanelType string `json:"panel_type,omitempty"` + UIDs []string `json:"uid,omitempty"` + Explain bool `json:"explain,omitempty"` // adds details on why document matched + WithAllowedActions bool `json:"withAllowedActions,omitempty"` // adds allowed actions per entity + Facet []FacetField `json:"facet,omitempty"` + SkipLocation bool `json:"skipLocation,omitempty"` + HasPreview string `json:"hasPreview,omitempty"` // the light|dark theme + Limit int `json:"limit,omitempty"` // explicit page size + From int `json:"from,omitempty"` // for paging +} + +type IsSearchReadyResponse struct { + IsReady bool + Reason string // initial-indexing-ongoing, org-indexing-ongoing +} + +type SearchService interface { + registry.CanBeDisabled + registry.BackgroundService + DoQuery(ctx context.Context, user *backend.User, orgId int64, query Query) *backend.DataResponse + doQuery(ctx context.Context, user *user.SignedInUser, orgId int64, query Query) *backend.DataResponse + IsReady(ctx context.Context, orgId int64) IsSearchReadyResponse + // RegisterDashboardIndexExtender(ext DashboardIndexExtender) + TriggerReIndex() +} diff --git a/pkg/storage/unified/resource/index.go b/pkg/storage/unified/resource/index.go index 39886ad875c..696ea11147f 100644 --- a/pkg/storage/unified/resource/index.go +++ b/pkg/storage/unified/resource/index.go @@ -8,6 +8,7 @@ import ( "os" "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/analysis/lang/en" "github.com/blevesearch/bleve/v2/mapping" "github.com/google/uuid" ) @@ -52,7 +53,13 @@ func (i *Index) Init(ctx context.Context) error { if err != nil { return err } - err = shard.batch.Index(res.Metadata.Uid, obj) + + var jsonDoc interface{} + err = json.Unmarshal(obj.Value, &jsonDoc) + if err != nil { + return err + } + err = shard.batch.Index(res.Metadata.Uid, jsonDoc) if err != nil { return err } @@ -99,6 +106,31 @@ func (i *Index) Delete(ctx context.Context, uid string, key *ResourceKey) error return nil } +func (i *Index) Search(ctx context.Context, tenant string, query string) ([]string, error) { + if tenant == "" { + tenant = "default" + } + shard, err := i.getShard(tenant) + if err != nil { + return nil, err + } + req := bleve.NewSearchRequest(bleve.NewQueryStringQuery(query)) + req.Fields = []string{"kind", "spec.title"} + + res, err := shard.index.Search(req) + if err != nil { + return nil, err + } + + hits := res.Hits + results := []string{} + for _, hit := range hits { + val := fmt.Sprintf("%s:%s", hit.Fields["kind"], hit.Fields["spec.title"]) + results = append(results, val) + } + return results, nil +} + func tenant(res *Resource) string { return res.Metadata.Namespace } @@ -142,20 +174,31 @@ func createIndexMappings() *mapping.IndexMappingImpl { metaMapping.AddFieldMappingsAt("name", nameFieldMapping) metaMapping.AddFieldMappingsAt("creationTimestamp", creationTimestampFieldMapping) metaMapping.Dynamic = false + metaMapping.Enabled = true + + specMapping := bleve.NewDocumentMapping() + specMapping.AddFieldMappingsAt("title", nameFieldMapping) + specMapping.Dynamic = false + specMapping.Enabled = true //Create a sub-document mapping for the metadata field objectMapping := bleve.NewDocumentMapping() objectMapping.AddSubDocumentMapping("metadata", metaMapping) + objectMapping.AddSubDocumentMapping("spec", specMapping) + objectMapping.Dynamic = false + objectMapping.Enabled = true + + // a generic reusable mapping for english text + englishTextFieldMapping := bleve.NewTextFieldMapping() + englishTextFieldMapping.Analyzer = en.AnalyzerName // Map top level fields - just kind for now - kindFieldMapping := bleve.NewTextFieldMapping() - objectMapping.AddFieldMappingsAt("kind", kindFieldMapping) + objectMapping.AddFieldMappingsAt("kind", englishTextFieldMapping) objectMapping.Dynamic = false // Create the index mapping indexMapping := bleve.NewIndexMapping() indexMapping.DefaultMapping = objectMapping - indexMapping.DefaultMapping.Dynamic = false return indexMapping } diff --git a/pkg/storage/unified/resource/index_server.go b/pkg/storage/unified/resource/index_server.go index ea45e335db8..c90f176549b 100644 --- a/pkg/storage/unified/resource/index_server.go +++ b/pkg/storage/unified/resource/index_server.go @@ -17,7 +17,14 @@ type IndexServer struct { } func (is IndexServer) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) { + results, err := is.index.Search(ctx, req.Tenant, req.Query) + if err != nil { + return nil, err + } res := &SearchResponse{} + for _, r := range results { + res.Items = append(res.Items, &ResourceWrapper{Value: []byte(r)}) + } return res, nil } diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go index 44e5be68f8c..7728dd880be 100644 --- a/pkg/storage/unified/resource/resource.pb.go +++ b/pkg/storage/unified/resource/resource.pb.go @@ -1619,7 +1619,8 @@ type SearchRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + Tenant string `protobuf:"bytes,2,opt,name=tenant,proto3" json:"tenant,omitempty"` } func (x *SearchRequest) Reset() { @@ -1661,6 +1662,13 @@ func (x *SearchRequest) GetQuery() string { return "" } +func (x *SearchRequest) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + type SearchResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2441,135 +2449,136 @@ var file_resource_proto_rawDesc = []byte{ 0x09, 0x0a, 0x05, 0x41, 0x44, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d, 0x41, 0x52, - 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x25, + 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x41, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 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, 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, 0xbf, 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, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 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, + 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x22, 0x41, 0x0a, + 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 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, + 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, 0xbf, 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, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 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, 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, 0xc4, 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, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, - 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, - 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, - 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, - 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, - 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x2a, 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, - 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, - 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, - 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, - 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, - 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, - 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0xc9, 0x01, 0x0a, 0x0d, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x3b, 0x0a, 0x06, - 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, - 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, - 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, - 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, - 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, - 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, - 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, - 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, - 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, - 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, - 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 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, 0xc4, 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, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, + 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, + 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, + 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, + 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, + 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, + 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x2a, 0x33, 0x0a, + 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, + 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, + 0x10, 0x01, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, + 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, + 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, + 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, + 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x30, 0x01, 0x32, 0xc9, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x12, 0x3b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x17, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, + 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, + 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, + 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto index 9859ea51994..dcaee25b909 100644 --- a/pkg/storage/unified/resource/resource.proto +++ b/pkg/storage/unified/resource/resource.proto @@ -326,6 +326,7 @@ message WatchEvent { message SearchRequest { string query = 1; + string tenant = 2; } message SearchResponse { diff --git a/pkg/tsdb/grafanads/grafana.go b/pkg/tsdb/grafanads/grafana.go index e3eb5c7f8bd..4a7cc09413b 100644 --- a/pkg/tsdb/grafanads/grafana.go +++ b/pkg/tsdb/grafanads/grafana.go @@ -15,8 +15,10 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/unifiedSearch" testdatasource "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource" ) @@ -51,15 +53,17 @@ var ( ) ) -func ProvideService(search searchV2.SearchService, store store.StorageService) *Service { - return newService(search, store) +func ProvideService(search searchV2.SearchService, searchNext unifiedSearch.SearchService, store store.StorageService, features featuremgmt.FeatureToggles) *Service { + return newService(search, searchNext, store, features) } -func newService(search searchV2.SearchService, store store.StorageService) *Service { +func newService(search searchV2.SearchService, searchNext unifiedSearch.SearchService, store store.StorageService, features featuremgmt.FeatureToggles) *Service { s := &Service{ - search: search, - store: store, - log: log.New("grafanads"), + search: search, + searchNext: searchNext, + store: store, + log: log.New("grafanads"), + features: features, } return s @@ -67,9 +71,11 @@ func newService(search searchV2.SearchService, store store.StorageService) *Serv // Service exists regardless of user settings type Service struct { - search searchV2.SearchService - store store.StorageService - log log.Logger + search searchV2.SearchService + searchNext unifiedSearch.SearchService + store store.StorageService + log log.Logger + features featuremgmt.FeatureToggles } func DataSourceModel(orgId int64) *datasources.DataSource { @@ -95,7 +101,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) response.Responses[q.RefID] = s.doListQuery(ctx, q) case queryTypeRead: response.Responses[q.RefID] = s.doReadQuery(ctx, q) - case queryTypeSearch: + case queryTypeSearch, queryTypeSearchNext: response.Responses[q.RefID] = s.doSearchQuery(ctx, req, q) default: response.Responses[q.RefID] = backend.DataResponse{ @@ -177,6 +183,18 @@ func (s *Service) doRandomWalk(query backend.DataQuery) backend.DataResponse { } func (s *Service) doSearchQuery(ctx context.Context, req *backend.QueryDataRequest, query backend.DataQuery) backend.DataResponse { + m := requestModel{} + err := json.Unmarshal(query.JSON, &m) + if err != nil { + return backend.DataResponse{ + Error: err, + } + } + + if s.features.IsEnabled(ctx, featuremgmt.FlagUnifiedStorageSearch) { + return *s.searchNext.DoQuery(ctx, req.PluginContext.User, req.PluginContext.OrgID, m.SearchNext) + } + searchReadinessCheckResp := s.search.IsReady(ctx, req.PluginContext.OrgID) if !searchReadinessCheckResp.IsReady { dashboardSearchNotServedRequestsCounter.With(prometheus.Labels{ @@ -192,17 +210,11 @@ func (s *Service) doSearchQuery(ctx context.Context, req *backend.QueryDataReque } } - m := requestModel{} - err := json.Unmarshal(query.JSON, &m) - if err != nil { - return backend.DataResponse{ - Error: err, - } - } return *s.search.DoDashboardQuery(ctx, req.PluginContext.User, req.PluginContext.OrgID, m.Search) } type requestModel struct { - QueryType string `json:"queryType"` - Search searchV2.DashboardQuery `json:"search,omitempty"` + QueryType string `json:"queryType"` + Search searchV2.DashboardQuery `json:"search,omitempty"` + SearchNext unifiedSearch.Query `json:"searchNext,omitempty"` } diff --git a/pkg/tsdb/grafanads/query.go b/pkg/tsdb/grafanads/query.go index 8abaf7b5f8d..af0b7afdd7f 100644 --- a/pkg/tsdb/grafanads/query.go +++ b/pkg/tsdb/grafanads/query.go @@ -7,6 +7,9 @@ const ( // QueryTypeList will list the files in a folder queryTypeSearch = "search" + // queryTypeSearchNext will perform a search query using the next generation search service + queryTypeSearchNext = "searchNext" + // QueryTypeList will list the files in a folder queryTypeList = "list" diff --git a/public/app/features/search/service/searcher.ts b/public/app/features/search/service/searcher.ts index 354411534c9..b7e8b885424 100644 --- a/public/app/features/search/service/searcher.ts +++ b/public/app/features/search/service/searcher.ts @@ -4,6 +4,7 @@ import { BlugeSearcher } from './bluge'; import { FrontendSearcher } from './frontend'; import { SQLSearcher } from './sql'; import { GrafanaSearcher } from './types'; +import { UnifiedSearcher } from './unified'; let searcher: GrafanaSearcher | undefined = undefined; @@ -13,6 +14,11 @@ export function getGrafanaSearcher(): GrafanaSearcher { const useBluge = config.featureToggles.panelTitleSearch; searcher = useBluge ? new BlugeSearcher(sqlSearcher) : sqlSearcher; + const useUnified = config.featureToggles.unifiedStorageSearch; + if (useUnified) { + searcher = new UnifiedSearcher(sqlSearcher); + } + if (useBluge && location.search.includes('do-frontend-query')) { searcher = new FrontendSearcher(searcher); } diff --git a/public/app/features/search/service/unified.ts b/public/app/features/search/service/unified.ts new file mode 100644 index 00000000000..882b3a0a5bc --- /dev/null +++ b/public/app/features/search/service/unified.ts @@ -0,0 +1,265 @@ +// TODO: fix - copied from bluge.ts +import { + DataFrame, + DataFrameJSON, + DataFrameView, + getDisplayProcessor, + SelectableValue, + toDataFrame, +} from '@grafana/data'; +import { config, getBackendSrv } from '@grafana/runtime'; +import { TermCount } from 'app/core/components/TagFilter/TagFilter'; + +import { replaceCurrentFolderQuery } from './utils'; + +import { DashboardQueryResult, GrafanaSearcher, QueryResponse, SearchQuery } from '.'; + +// The backend returns an empty frame with a special name to indicate that the indexing engine is being rebuilt, +// and that it can not serve any search requests. We are temporarily using the old SQL Search API as a fallback when that happens. +const loadingFrameName = 'Loading'; + +const searchURI = 'api/unified-search'; + +type SearchAPIResponse = { + frames: DataFrameJSON[]; +}; + +const folderViewSort = 'name_sort'; + +export class UnifiedSearcher implements GrafanaSearcher { + constructor(private fallbackSearcher: GrafanaSearcher) {} + + async search(query: SearchQuery): Promise { + if (query.facet?.length) { + throw new Error('facets not supported!'); + } + return this.doSearchQuery(query); + } + + // TODO: fix - copied from bluge.ts + async starred(query: SearchQuery): Promise { + if (query.facet?.length) { + throw new Error('facets not supported!'); + } + // get the starred dashboards + const starsUIDS = await getBackendSrv().get('api/user/stars'); + if (starsUIDS?.length) { + return this.doSearchQuery({ + uid: starsUIDS, + query: query.query ?? '*', + }); + } + // Nothing is starred + return { + view: new DataFrameView({ length: 0, fields: [] }), + totalRows: 0, + loadMoreItems: async (startIndex: number, stopIndex: number): Promise => { + return; + }, + isItemLoaded: (index: number): boolean => { + return true; + }, + }; + } + + // TODO: fix - copied from bluge.ts + async tags(query: SearchQuery): Promise { + const req = { + ...query, + query: query.query ?? '*', + sort: undefined, // no need to sort the initial query results (not used) + facet: [{ field: 'tag' }], + limit: 1, // 0 would be better, but is ignored by the backend + }; + + const resp = await getBackendSrv().post(searchURI, req); + const frames = resp.frames.map((f) => toDataFrame(f)); + + if (frames[0]?.name === loadingFrameName) { + return this.fallbackSearcher.tags(query); + } + + for (const frame of frames) { + if (frame.fields[0].name === 'tag') { + return getTermCountsFrom(frame); + } + } + + return []; + } + + // TODO: fix - copied from bluge.ts + getSortOptions(): Promise { + const opts: SelectableValue[] = [ + { value: folderViewSort, label: 'Alphabetically (A-Z)' }, + { value: '-name_sort', label: 'Alphabetically (Z-A)' }, + ]; + + if (config.licenseInfo.enabledFeatures.analytics) { + for (const sf of sortFields) { + opts.push({ value: `-${sf.name}`, label: `${sf.display} (most)` }); + opts.push({ value: `${sf.name}`, label: `${sf.display} (least)` }); + } + for (const sf of sortTimeFields) { + opts.push({ value: `-${sf.name}`, label: `${sf.display} (recent)` }); + opts.push({ value: `${sf.name}`, label: `${sf.display} (oldest)` }); + } + } + + return Promise.resolve(opts); + } + + // TODO: update - copied from bluge.ts + async doSearchQuery(query: SearchQuery): Promise { + query = await replaceCurrentFolderQuery(query); + const req = { + ...query, + query: query.query ?? '*', + limit: query.limit ?? firstPageSize, + }; + + const rsp = await getBackendSrv().post(searchURI, req); + const frames = rsp.frames.map((f) => toDataFrame(f)); + + const first = frames.length ? toDataFrame(frames[0]) : { fields: [], length: 0 }; + + if (first.name === loadingFrameName) { + return this.fallbackSearcher.search(query); + } + + for (const field of first.fields) { + field.display = getDisplayProcessor({ field, theme: config.theme2 }); + } + + // Make sure the object exists + if (!first.meta?.custom) { + first.meta = { + ...first.meta, + custom: { + count: first.length, + max_score: 1, + }, + }; + } + + const meta = first.meta.custom || {}; + if (!meta.locationInfo) { + meta.locationInfo = {}; // always set it so we can append + } + + // Set the field name to a better display name + if (meta.sortBy?.length) { + const field = first.fields.find((f) => f.name === meta.sortBy); + if (field) { + const name = getSortFieldDisplayName(field.name); + meta.sortBy = name; + field.name = name; // make it look nicer + } + } + + let loadMax = 0; + let pending: Promise | undefined = undefined; + const getNextPage = async () => { + while (loadMax > view.dataFrame.length) { + const from = view.dataFrame.length; + if (from >= meta.count) { + return; + } + const resp = await getBackendSrv().post(searchURI, { + ...(req ?? {}), + from, + limit: nextPageSizes, + }); + const frame = toDataFrame(resp.frames[0]); + + if (!frame) { + console.log('no results', frame); + return; + } + if (frame.fields.length !== view.dataFrame.fields.length) { + console.log('invalid shape', frame, view.dataFrame); + return; + } + + // Append the raw values to the same array buffer + const length = frame.length + view.dataFrame.length; + for (let i = 0; i < frame.fields.length; i++) { + const values = view.dataFrame.fields[i].values; + values.push(...frame.fields[i].values); + } + view.dataFrame.length = length; + + // Add all the location lookup info + const submeta = frame.meta?.custom; + if (submeta?.locationInfo && meta) { + for (const [key, value] of Object.entries(submeta.locationInfo)) { + meta.locationInfo[key] = value; + } + } + } + pending = undefined; + }; + + const view = new DataFrameView(first); + return { + totalRows: meta.count ?? first.length, + view, + loadMoreItems: async (startIndex: number, stopIndex: number): Promise => { + loadMax = Math.max(loadMax, stopIndex); + if (!pending) { + pending = getNextPage(); + } + return pending; + }, + isItemLoaded: (index: number): boolean => { + return index < view.dataFrame.length; + }, + }; + } + + getFolderViewSort(): string { + return 'name_sort'; + } +} + +const firstPageSize = 50; +const nextPageSizes = 100; + +function getTermCountsFrom(frame: DataFrame): TermCount[] { + const keys = frame.fields[0].values; + const vals = frame.fields[1].values; + const counts: TermCount[] = []; + for (let i = 0; i < frame.length; i++) { + counts.push({ term: keys[i], count: vals[i] }); + } + return counts; +} + +// Enterprise only sort field values for dashboards +const sortFields = [ + { name: 'views_total', display: 'Views total' }, + { name: 'views_last_30_days', display: 'Views 30 days' }, + { name: 'errors_total', display: 'Errors total' }, + { name: 'errors_last_30_days', display: 'Errors 30 days' }, +]; + +// Enterprise only time sort field values for dashboards +const sortTimeFields = [ + { name: 'created_at', display: 'Created time' }, + { name: 'updated_at', display: 'Updated time' }, +]; + +/** Given the internal field name, this gives a reasonable display name for the table colum header */ +function getSortFieldDisplayName(name: string) { + for (const sf of sortFields) { + if (sf.name === name) { + return sf.display; + } + } + for (const sf of sortTimeFields) { + if (sf.name === name) { + return sf.display; + } + } + return name; +} diff --git a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx index e5f3b948d1c..2c473402942 100644 --- a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx @@ -82,6 +82,13 @@ export class UnthemedQueryEditor extends PureComponent { description: 'Search for grafana resources', }); } + if (config.featureToggles.unifiedStorageSearch) { + this.queryTypes.push({ + label: 'Search (experimental)', + value: GrafanaQueryType.SearchNext, + description: 'Search for grafana resources', + }); + } if (config.featureToggles.editPanelCSVDragAndDrop) { this.queryTypes.push({ label: 'Spreadsheet or snapshot', @@ -432,6 +439,16 @@ export class UnthemedQueryEditor extends PureComponent { onRunQuery(); }; + onSearchNextChange = (search: SearchQuery) => { + const { query, onChange, onRunQuery } = this.props; + + onChange({ + ...query, + searchNext: search, + }); + onRunQuery(); + }; + render() { const query = { ...defaultQuery, @@ -475,6 +492,9 @@ export class UnthemedQueryEditor extends PureComponent { {queryType === GrafanaQueryType.Search && ( )} + {queryType === GrafanaQueryType.SearchNext && ( + + )} ); } diff --git a/public/app/plugins/datasource/grafana/types.ts b/public/app/plugins/datasource/grafana/types.ts index a6680231dad..cb00f090443 100644 --- a/public/app/plugins/datasource/grafana/types.ts +++ b/public/app/plugins/datasource/grafana/types.ts @@ -19,6 +19,7 @@ export enum GrafanaQueryType { List = 'list', Read = 'read', Search = 'search', + SearchNext = 'searchNext', } export interface GrafanaQuery extends DataQuery { @@ -28,6 +29,7 @@ export interface GrafanaQuery extends DataQuery { buffer?: number; path?: string; // for list and read search?: SearchQuery; + searchNext?: SearchQuery; snapshot?: DataFrameJSON[]; timeRegion?: TimeRegionConfig; file?: GrafanaQueryFile;