From 5f675c26196c864a3912eea1185c8b1b203cb50c Mon Sep 17 00:00:00 2001 From: Johan Brandhorst-Satzkorn Date: Mon, 30 Oct 2023 14:41:12 -0700 Subject: [PATCH] handlers/hosts: implement pagination for hosts Adds protobuf types and handler logic to support pagination of hosts. --- internal/daemon/controller/handler.go | 2 +- .../controller/handlers/hosts/host_service.go | 268 +++++-- .../handlers/hosts/host_service_test.go | 721 +++++++++++++++--- internal/gen/controller.swagger.json | 43 ++ .../api/services/host_service.pb.go | 283 ++++--- .../api/services/v1/host_service.proto | 29 + 6 files changed, 1075 insertions(+), 271 deletions(-) diff --git a/internal/daemon/controller/handler.go b/internal/daemon/controller/handler.go index 07107f404f..a026da609d 100644 --- a/internal/daemon/controller/handler.go +++ b/internal/daemon/controller/handler.go @@ -150,7 +150,7 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error { services.RegisterHostSetServiceServer(s, hss) } if _, ok := currentServices[services.HostService_ServiceDesc.ServiceName]; !ok { - hs, err := hosts.NewService(c.baseContext, c.StaticHostRepoFn, c.PluginHostRepoFn) + hs, err := hosts.NewService(c.baseContext, c.StaticHostRepoFn, c.PluginHostRepoFn, c.conf.RawConfig.Controller.MaxPageSize) if err != nil { return fmt.Errorf("failed to create host handler service: %w", err) } diff --git a/internal/daemon/controller/handlers/hosts/host_service.go b/internal/daemon/controller/handlers/hosts/host_service.go index 2617a5e422..8ff2169267 100644 --- a/internal/daemon/controller/handlers/hosts/host_service.go +++ b/internal/daemon/controller/handlers/hosts/host_service.go @@ -19,12 +19,15 @@ import ( hostplugin "github.com/hashicorp/boundary/internal/host/plugin" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/host/static/store" + "github.com/hashicorp/boundary/internal/listtoken" + "github.com/hashicorp/boundary/internal/pagination" "github.com/hashicorp/boundary/internal/perms" plugin "github.com/hashicorp/boundary/internal/plugin" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/subtypes" + "github.com/hashicorp/boundary/internal/util" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hosts" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/plugins" "golang.org/x/exp/maps" @@ -58,8 +61,6 @@ var ( ) ) -const domain = "host" - func init() { var err error if maskManager, err = handlers.NewMaskManager( @@ -79,13 +80,14 @@ type Service struct { staticRepoFn common.StaticRepoFactory pluginRepoFn common.PluginHostRepoFactory + maxPageSize uint } var _ pbs.HostServiceServer = (*Service)(nil) // NewService returns a host Service which handles host related requests to boundary and uses the provided // repositories for storage and retrieval. -func NewService(ctx context.Context, repoFn common.StaticRepoFactory, pluginRepoFn common.PluginHostRepoFactory) (Service, error) { +func NewService(ctx context.Context, repoFn common.StaticRepoFactory, pluginRepoFn common.PluginHostRepoFactory, maxPageSize uint) (Service, error) { const op = "hosts.NewService" if repoFn == nil { return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing static repository") @@ -93,73 +95,188 @@ func NewService(ctx context.Context, repoFn common.StaticRepoFactory, pluginRepo if pluginRepoFn == nil { return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing plugin host repository") } - return Service{staticRepoFn: repoFn, pluginRepoFn: pluginRepoFn}, nil + if maxPageSize == 0 { + maxPageSize = uint(globals.DefaultMaxPageSize) + } + return Service{staticRepoFn: repoFn, pluginRepoFn: pluginRepoFn, maxPageSize: maxPageSize}, nil } func (s Service) ListHosts(ctx context.Context, req *pbs.ListHostsRequest) (*pbs.ListHostsResponse, error) { + const op = "hosts.(Service).ListHosts" if err := validateListRequest(ctx, req); err != nil { - return nil, err + return nil, errors.Wrap(ctx, err, op) } _, authResults := s.parentAndAuthResult(ctx, req.GetHostCatalogId(), action.List) if authResults.Error != nil { return nil, authResults.Error } - hl, plg, err := s.listFromRepo(ctx, req.GetHostCatalogId()) - if err != nil { - return nil, err + pageSize := int(s.maxPageSize) + // Use the requested page size only if it is smaller than + // the configured max. + if req.GetPageSize() != 0 && uint(req.GetPageSize()) < s.maxPageSize { + pageSize = int(req.GetPageSize()) } - if len(hl) == 0 { - return &pbs.ListHostsResponse{}, nil + + var filterItemFn func(ctx context.Context, item host.Host, plg *plugin.Plugin) (bool, error) + switch { + case req.GetFilter() != "": + // Only use a filter if we need to + filter, err := handlers.NewFilter(ctx, req.GetFilter()) + if err != nil { + return nil, err + } + // TODO: replace the need for this function with some way to convert the `filter` + // to a domain type. This would allow filtering to happen in the domain, and we could + // remove this callback altogether. + filterItemFn = func(ctx context.Context, item host.Host, plg *plugin.Plugin) (bool, error) { + outputOpts, ok := newOutputOpts(ctx, item, plg, authResults) + if !ok { + return false, nil + } + pbItem, err := toProto(ctx, item, outputOpts...) + if err != nil { + return false, err + } + + filterable, err := subtypes.Filterable(ctx, pbItem) + if err != nil { + return false, err + } + return filter.Match(filterable), nil + } + default: + filterItemFn = func(ctx context.Context, item host.Host, plg *plugin.Plugin) (bool, error) { + return true, nil + } } - filter, err := handlers.NewFilter(ctx, req.GetFilter()) + grantsHash, err := authResults.GrantsHash(ctx) if err != nil { return nil, err } - finalItems := make([]*pb.Host, 0, len(hl)) - res := perms.Resource{ - ScopeId: authResults.Scope.Id, - Type: resource.Host, - Pin: req.GetHostCatalogId(), - } - for _, item := range hl { - res.Id = item.GetPublicId() - idActions := idActionsTypeMap[globals.ResourceInfoFromPrefix(res.Id).Subtype] - authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), idActions, auth.WithResource(&res)).Strings() - if len(authorizedActions) == 0 { - continue + var listResp *pagination.ListResponse[host.Host] + var sortBy string + var plg *plugin.Plugin + switch globals.ResourceInfoFromPrefix(req.GetHostCatalogId()).Subtype { + case static.Subtype: + // Wrap the filter item func that takes a plugin, since the static host + // domain does not use a plugin. + staticFilterItemFn := func(ctx context.Context, item host.Host) (bool, error) { + return filterItemFn(ctx, item, nil) } - - outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(authResults.UserId) - outputOpts := make([]handlers.Option, 0, 3) - outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields)) - if plg != nil { - outputOpts = append(outputOpts, handlers.WithPlugin(plg)) + repo, err := s.staticRepoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if req.GetListToken() == "" { + sortBy = "created_time" + listResp, err = static.ListHosts(ctx, grantsHash, pageSize, staticFilterItemFn, repo, req.GetHostCatalogId()) + if err != nil { + return nil, err + } + } else { + listToken, err := handlers.ParseListToken(ctx, req.GetListToken(), resource.Host, grantsHash) + if err != nil { + return nil, err + } + switch st := listToken.Subtype.(type) { + case *listtoken.PaginationToken: + sortBy = "created_time" + listResp, err = static.ListHostsPage(ctx, grantsHash, pageSize, staticFilterItemFn, listToken, repo, req.GetHostCatalogId()) + if err != nil { + return nil, err + } + case *listtoken.StartRefreshToken: + sortBy = "updated_time" + listResp, err = static.ListHostsRefresh(ctx, grantsHash, pageSize, staticFilterItemFn, listToken, repo, req.GetHostCatalogId()) + if err != nil { + return nil, err + } + case *listtoken.RefreshToken: + sortBy = "updated_time" + listResp, err = static.ListHostsRefreshPage(ctx, grantsHash, pageSize, staticFilterItemFn, listToken, repo, req.GetHostCatalogId()) + if err != nil { + return nil, err + } + default: + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "unexpected list token subtype: %T", st) + } } - if outputFields.Has(globals.ScopeField) { - outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope)) + case hostplugin.Subtype: + repo, err := s.pluginRepoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if req.GetListToken() == "" { + sortBy = "created_time" + listResp, plg, err = hostplugin.ListHosts(ctx, grantsHash, pageSize, filterItemFn, repo, req.GetHostCatalogId()) + if err != nil { + return nil, err + } + } else { + listToken, err := handlers.ParseListToken(ctx, req.GetListToken(), resource.Host, grantsHash) + if err != nil { + return nil, err + } + switch st := listToken.Subtype.(type) { + case *listtoken.PaginationToken: + sortBy = "created_time" + listResp, plg, err = hostplugin.ListHostsPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, req.GetHostCatalogId()) + if err != nil { + return nil, err + } + case *listtoken.StartRefreshToken: + sortBy = "updated_time" + listResp, plg, err = hostplugin.ListHostsRefresh(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, req.GetHostCatalogId()) + if err != nil { + return nil, err + } + case *listtoken.RefreshToken: + sortBy = "updated_time" + listResp, plg, err = hostplugin.ListHostsRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, req.GetHostCatalogId()) + if err != nil { + return nil, err + } + default: + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "unexpected list token subtype: %T", st) + } } - if outputFields.Has(globals.AuthorizedActionsField) { - outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } + + finalItems := make([]*pb.Host, 0, len(listResp.Items)) + for _, item := range listResp.Items { + outputOpts, ok := newOutputOpts(ctx, item, plg, authResults) + if !ok { + continue } - outputOpts = append(outputOpts, handlers.WithHostSetIds(item.GetSetIds())) - item, err := toProto(ctx, item, outputOpts...) + pbItem, err := toProto(ctx, item, outputOpts...) if err != nil { - return nil, err + continue } + finalItems = append(finalItems, pbItem) + } + respType := "delta" + if listResp.CompleteListing { + respType = "complete" + } + resp := &pbs.ListHostsResponse{ + Items: finalItems, + EstItemCount: uint32(listResp.EstimatedItemCount), + RemovedIds: listResp.DeletedIds, + ResponseType: respType, + SortBy: sortBy, + SortDir: "desc", + } - // This comes last so that we can use item fields in the filter after - // the allowed fields are populated above - filterable, err := subtypes.Filterable(ctx, item) + if listResp.ListToken != nil { + resp.ListToken, err = handlers.MarshalListToken(ctx, listResp.ListToken, pbs.ResourceType_RESOURCE_TYPE_HOST) if err != nil { return nil, err } - if filter.Match(filterable) { - finalItems = append(finalItems, item) - } } - return &pbs.ListHostsResponse{Items: finalItems}, nil + + return resp, nil } // GetHost implements the interface pbs.HostServiceServer. @@ -315,7 +432,7 @@ func (s Service) getFromRepo(ctx context.Context, id string) (host.Host, *plugin if err != nil { return nil, nil, err } - if h == nil { + if util.IsNil(h) { return nil, nil, handlers.NotFoundErrorf("Host %q doesn't exist.", id) } case hostplugin.Subtype: @@ -417,39 +534,6 @@ func (s Service) deleteFromRepo(ctx context.Context, projectId, id string) (bool return rows > 0, nil } -func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]host.Host, *plugins.PluginInfo, error) { - var hosts []host.Host - var plg *plugins.PluginInfo - switch globals.ResourceInfoFromPrefix(catalogId).Subtype { - case static.Subtype: - repo, err := s.staticRepoFn() - if err != nil { - return nil, nil, err - } - hl, err := repo.ListHosts(ctx, catalogId, static.WithLimit(-1)) - if err != nil { - return nil, nil, err - } - for _, h := range hl { - hosts = append(hosts, h) - } - case hostplugin.Subtype: - repo, err := s.pluginRepoFn() - if err != nil { - return nil, nil, err - } - hl, hlPlg, err := repo.ListHostsByCatalogId(ctx, catalogId) - if err != nil { - return nil, nil, err - } - for _, h := range hl { - hosts = append(hosts, h) - } - plg = toPluginInfo(hlPlg) - } - return hosts, plg, nil -} - func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (host.Catalog, auth.VerifyResults) { res := auth.VerifyResults{} staticRepo, err := s.staticRepoFn() @@ -536,6 +620,36 @@ func toPluginInfo(plg *plugin.Plugin) *plugins.PluginInfo { } } +func newOutputOpts(ctx context.Context, item host.Host, plg *plugin.Plugin, authResults auth.VerifyResults) ([]handlers.Option, bool) { + res := perms.Resource{ + ScopeId: authResults.Scope.Id, + Type: resource.Host, + Pin: item.GetCatalogId(), + Id: item.GetPublicId(), + } + res.Id = item.GetPublicId() + idActions := idActionsTypeMap[globals.ResourceInfoFromPrefix(res.Id).Subtype] + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), idActions, auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { + return nil, false + } + + outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(authResults.UserId) + outputOpts := make([]handlers.Option, 0, 3) + outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields)) + if plg != nil { + outputOpts = append(outputOpts, handlers.WithPlugin(toPluginInfo(plg))) + } + if outputFields.Has(globals.ScopeField) { + outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope)) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } + outputOpts = append(outputOpts, handlers.WithHostSetIds(item.GetSetIds())) + return outputOpts, true +} + func toProto(ctx context.Context, in host.Host, opt ...handlers.Option) (*pb.Host, error) { opts := handlers.GetOpts(opt...) if opts.WithOutputFields == nil { diff --git a/internal/daemon/controller/handlers/hosts/host_service_test.go b/internal/daemon/controller/handlers/hosts/host_service_test.go index 8c2a02123c..25d1e16b55 100644 --- a/internal/daemon/controller/handlers/hosts/host_service_test.go +++ b/internal/daemon/controller/handlers/hosts/host_service_test.go @@ -7,7 +7,7 @@ import ( "context" "errors" "fmt" - "sort" + "slices" "strings" "testing" @@ -15,18 +15,24 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/daemon/controller/auth" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/hosts" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" + "github.com/hashicorp/boundary/internal/host" hostplugin "github.com/hashicorp/boundary/internal/host/plugin" "github.com/hashicorp/boundary/internal/host/static" + "github.com/hashicorp/boundary/internal/host/static/store" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/plugin" "github.com/hashicorp/boundary/internal/plugin/loopback" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/scheduler" + "github.com/hashicorp/boundary/internal/server" "github.com/hashicorp/boundary/internal/types/scope" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hosts" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/plugins" @@ -47,6 +53,46 @@ var testAuthorizedActions = map[globals.Subtype][]string{ hostplugin.Subtype: {"no-op", "read"}, } +func staticHostToProto(h host.Host, proj *iam.Scope, hostSet *static.HostSet) *pb.Host { + return &pb.Host{ + Id: h.GetPublicId(), + HostCatalogId: h.GetCatalogId(), + Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: proj.GetParentId()}, + CreatedTime: h.GetCreateTime().GetTimestamp(), + UpdatedTime: h.GetUpdateTime().GetTimestamp(), + Version: h.GetVersion(), + Type: static.Subtype.String(), + Attrs: &pb.Host_StaticHostAttributes{ + StaticHostAttributes: &pb.StaticHostAttributes{ + Address: wrapperspb.String(h.GetAddress()), + }, + }, + AuthorizedActions: testAuthorizedActions[static.Subtype], + HostSetIds: []string{hostSet.GetPublicId()}, + } +} + +func pluginHostToProto(h host.Host, proj *iam.Scope, hostSet *hostplugin.HostSet, plg *plugin.Plugin, extId string, extName string) *pb.Host { + return &pb.Host{ + Id: h.GetPublicId(), + HostCatalogId: h.GetCatalogId(), + Plugin: &plugins.PluginInfo{ + Id: plg.GetPublicId(), + Name: plg.GetName(), + Description: plg.GetDescription(), + }, + Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: proj.GetParentId()}, + CreatedTime: h.GetCreateTime().GetTimestamp(), + UpdatedTime: h.GetUpdateTime().GetTimestamp(), + HostSetIds: []string{hostSet.GetPublicId()}, + Version: 1, + ExternalId: extId, + ExternalName: extName, + Type: hostplugin.Subtype.String(), + AuthorizedActions: testAuthorizedActions[hostplugin.Subtype], + } +} + func TestGet_Static(t *testing.T) { t.Parallel() ctx := context.Background() @@ -59,7 +105,7 @@ func TestGet_Static(t *testing.T) { return iamRepo, nil } - org, proj := iam.TestScopes(t, iamRepo) + _, proj := iam.TestScopes(t, iamRepo) rw := db.New(conn) sche := scheduler.TestScheduler(t, conn, wrapper) @@ -74,21 +120,7 @@ func TestGet_Static(t *testing.T) { s := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] static.TestSetMembers(t, conn, s.GetPublicId(), []*static.Host{h}) - pHost := &pb.Host{ - HostCatalogId: hc.GetPublicId(), - Id: h.GetPublicId(), - CreatedTime: h.CreateTime.GetTimestamp(), - UpdatedTime: h.UpdateTime.GetTimestamp(), - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, - Type: "static", - Attrs: &pb.Host_StaticHostAttributes{ - StaticHostAttributes: &pb.StaticHostAttributes{ - Address: wrapperspb.String(h.GetAddress()), - }, - }, - AuthorizedActions: testAuthorizedActions[static.Subtype], - HostSetIds: []string{s.GetPublicId()}, - } + pHost := staticHostToProto(h, proj, s) cases := []struct { name string @@ -123,7 +155,7 @@ func TestGet_Static(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := hosts.NewService(ctx, repoFn, pluginRepoFn) + s, err := hosts.NewService(ctx, repoFn, pluginRepoFn, 1000) require.NoError(err, "Couldn't create a new host service.") got, gErr := s.GetHost(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req) @@ -159,7 +191,7 @@ func TestGet_Plugin(t *testing.T) { return iamRepo, nil } - org, proj := iam.TestScopes(t, iamRepo) + _, proj := iam.TestScopes(t, iamRepo) plg := plugin.TestPlugin(t, conn, "test") plgm := map[string]plgpb.HostPluginServiceClient{ @@ -183,23 +215,7 @@ func TestGet_Plugin(t *testing.T) { hs := hostplugin.TestSet(t, conn, kms, sche, hc, plgm) hostplugin.TestSetMembers(t, conn, hs.GetPublicId(), []*hostplugin.Host{h, hPrev}) - pHost := &pb.Host{ - HostCatalogId: hc.GetPublicId(), - Id: h.GetPublicId(), - CreatedTime: h.CreateTime.GetTimestamp(), - UpdatedTime: h.UpdateTime.GetTimestamp(), - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, - Type: hostplugin.Subtype.String(), - Plugin: &plugins.PluginInfo{ - Id: plg.GetPublicId(), - Name: plg.GetName(), - Description: plg.GetDescription(), - }, - HostSetIds: []string{hs.GetPublicId()}, - ExternalId: "test", - ExternalName: "test-ext-name", - AuthorizedActions: testAuthorizedActions[hostplugin.Subtype], - } + pHost := pluginHostToProto(h, proj, hs, plg, "test", "test-ext-name") cases := []struct { name string @@ -247,7 +263,7 @@ func TestGet_Plugin(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := hosts.NewService(ctx, repoFn, pluginRepoFn) + s, err := hosts.NewService(ctx, repoFn, pluginRepoFn, 1000) require.NoError(err, "Couldn't create a new host service.") got, gErr := s.GetHost(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req) @@ -284,7 +300,7 @@ func TestList_Static(t *testing.T) { return iamRepo, nil } - org, proj := iam.TestScopes(t, iamRepo) + _, proj := iam.TestScopes(t, iamRepo) rw := db.New(conn) sche := scheduler.TestScheduler(t, conn, wrapper) @@ -302,26 +318,10 @@ func TestList_Static(t *testing.T) { testHosts := static.TestHosts(t, conn, hc.GetPublicId(), 10) static.TestSetMembers(t, conn, hset.GetPublicId(), testHosts) for _, h := range testHosts { - wantHs = append(wantHs, &pb.Host{ - Id: h.GetPublicId(), - HostCatalogId: h.GetCatalogId(), - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, - CreatedTime: h.GetCreateTime().GetTimestamp(), - UpdatedTime: h.GetUpdateTime().GetTimestamp(), - Version: h.GetVersion(), - Type: static.Subtype.String(), - Attrs: &pb.Host_StaticHostAttributes{ - StaticHostAttributes: &pb.StaticHostAttributes{ - Address: wrapperspb.String(h.GetAddress()), - }, - }, - AuthorizedActions: testAuthorizedActions[static.Subtype], - HostSetIds: []string{hset.GetPublicId()}, - }) + wantHs = append(wantHs, staticHostToProto(h, proj, hset)) } - sort.Slice(wantHs, func(i int, j int) bool { - return wantHs[i].GetId() < wantHs[j].GetId() - }) + + slices.Reverse(wantHs) cases := []struct { name string @@ -332,7 +332,13 @@ func TestList_Static(t *testing.T) { { name: "List Many Hosts", req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId()}, - res: &pbs.ListHostsResponse{Items: wantHs}, + res: &pbs.ListHostsResponse{ + Items: wantHs, + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + EstItemCount: 10, + }, }, { name: "List Non Existing Hosts", @@ -342,17 +348,31 @@ func TestList_Static(t *testing.T) { { name: "List No Hosts", req: &pbs.ListHostsRequest{HostCatalogId: hcNoHosts.GetPublicId()}, - res: &pbs.ListHostsResponse{}, + res: &pbs.ListHostsResponse{ + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + }, }, { name: "Filter to One Host", req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId(), Filter: fmt.Sprintf(`"/item/id"==%q`, wantHs[1].GetId())}, - res: &pbs.ListHostsResponse{Items: wantHs[1:2]}, + res: &pbs.ListHostsResponse{ + Items: wantHs[1:2], + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + EstItemCount: 1, + }, }, { name: "Filter to No Hosts", req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId(), Filter: `"/item/id"=="doesnt match"`}, - res: &pbs.ListHostsResponse{}, + res: &pbs.ListHostsResponse{ + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + }, }, { name: "Filter Bad Format", @@ -363,7 +383,7 @@ func TestList_Static(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := hosts.NewService(ctx, repoFn, pluginRepoFn) + s, err := hosts.NewService(ctx, repoFn, pluginRepoFn, 1000) require.NoError(err, "Couldn't create new host set service.") // Test non-anonymous listing @@ -377,11 +397,12 @@ func TestList_Static(t *testing.T) { assert.Empty(cmp.Diff( got, tc.res, - protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), - ), "ListHosts(%q) got response %q, wanted %q", tc.req, got, tc.res) + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + )) // Test anonymous listing got, gErr = s.ListHosts(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -407,7 +428,7 @@ func TestList_Plugin(t *testing.T) { return iamRepo, nil } - org, proj := iam.TestScopes(t, iamRepo) + _, proj := iam.TestScopes(t, iamRepo) plg := plugin.TestPlugin(t, conn, "test") plgm := map[string]plgpb.HostPluginServiceClient{ plg.GetPublicId(): loopback.NewWrappingPluginHostClient(&loopback.TestPluginServer{}), @@ -427,31 +448,14 @@ func TestList_Plugin(t *testing.T) { var wantHs []*pb.Host for i := 0; i < 10; i++ { - extId := fmt.Sprintf("host %d", i) - h := hostplugin.TestHost(t, conn, hc.GetPublicId(), extId, hostplugin.WithExternalName(fmt.Sprintf("ext-name-%d", i))) + extId := fmt.Sprintf("ext-id-%d", i) + extName := fmt.Sprintf("ext-name-%d", i) + h := hostplugin.TestHost(t, conn, hc.GetPublicId(), extId, hostplugin.WithExternalName(extName)) hostplugin.TestSetMembers(t, conn, hs.GetPublicId(), []*hostplugin.Host{h}) - wantHs = append(wantHs, &pb.Host{ - Id: h.GetPublicId(), - HostCatalogId: h.GetCatalogId(), - Plugin: &plugins.PluginInfo{ - Id: plg.GetPublicId(), - Name: plg.GetName(), - Description: plg.GetDescription(), - }, - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, - CreatedTime: h.GetCreateTime().GetTimestamp(), - UpdatedTime: h.GetUpdateTime().GetTimestamp(), - HostSetIds: []string{hs.GetPublicId()}, - Version: 1, - ExternalId: extId, - ExternalName: fmt.Sprintf("ext-name-%d", i), - Type: hostplugin.Subtype.String(), - AuthorizedActions: testAuthorizedActions[hostplugin.Subtype], - }) + wantHs = append(wantHs, pluginHostToProto(h, proj, hs, plg, extId, extName)) } - sort.Slice(wantHs, func(i, j int) bool { - return wantHs[i].GetId() < wantHs[j].GetId() - }) + + slices.Reverse(wantHs) cases := []struct { name string @@ -462,7 +466,13 @@ func TestList_Plugin(t *testing.T) { { name: "List Many Hosts", req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId()}, - res: &pbs.ListHostsResponse{Items: wantHs}, + res: &pbs.ListHostsResponse{ + Items: wantHs, + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + EstItemCount: 10, + }, }, { name: "List Non Existing Hosts", @@ -472,17 +482,31 @@ func TestList_Plugin(t *testing.T) { { name: "List No Hosts", req: &pbs.ListHostsRequest{HostCatalogId: hcNoHosts.GetPublicId()}, - res: &pbs.ListHostsResponse{}, + res: &pbs.ListHostsResponse{ + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + }, }, { name: "Filter to One Host", req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId(), Filter: fmt.Sprintf(`"/item/id"==%q`, wantHs[1].GetId())}, - res: &pbs.ListHostsResponse{Items: wantHs[1:2]}, + res: &pbs.ListHostsResponse{ + Items: wantHs[1:2], + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + EstItemCount: 1, + }, }, { name: "Filter to No Hosts", req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId(), Filter: `"/item/id"=="doesnt match"`}, - res: &pbs.ListHostsResponse{}, + res: &pbs.ListHostsResponse{ + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + }, }, { name: "Filter Bad Format", @@ -493,7 +517,7 @@ func TestList_Plugin(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := hosts.NewService(ctx, repoFn, pluginRepoFn) + s, err := hosts.NewService(ctx, repoFn, pluginRepoFn, 1000) require.NoError(err, "Couldn't create new host set service.") // Test non-anonymous listing @@ -505,9 +529,6 @@ func TestList_Plugin(t *testing.T) { } require.NoError(gErr) - sort.Slice(got.Items, func(i, j int) bool { - return got.Items[i].GetId() < got.Items[j].GetId() - }) assert.Empty(cmp.Diff( got, tc.res, @@ -515,7 +536,8 @@ func TestList_Plugin(t *testing.T) { cmpopts.SortSlices(func(a, b string) bool { return a < b }), - ), "ListHosts(%q) got response %q, wanted %q", tc.req, got, tc.res) + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + )) // Test anonymous listing got, gErr = s.ListHosts(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -530,6 +552,501 @@ func TestList_Plugin(t *testing.T) { } } +func TestListPagination(t *testing.T) { + // Set database read timeout to avoid duplicates in response + oldReadTimeout := globals.RefreshReadLookbackDuration + globals.RefreshReadLookbackDuration = 0 + t.Cleanup(func() { + globals.RefreshReadLookbackDuration = oldReadTimeout + }) + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + sqlDB, err := conn.SqlDB(ctx) + require.NoError(t, err) + kms := kms.TestKms(t, conn, wrapper) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + + org, proj := iam.TestScopes(t, iamRepo) + plg := plugin.TestPlugin(t, conn, "test") + plgm := map[string]plgpb.HostPluginServiceClient{ + plg.GetPublicId(): loopback.NewWrappingPluginHostClient(&loopback.TestPluginServer{}), + } + + rw := db.New(conn) + sche := scheduler.TestScheduler(t, conn, wrapper) + tokenRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(ctx, rw, rw, kms) + } + serversRepoFn := func() (*server.Repository, error) { + return server.NewRepository(ctx, rw, rw, kms) + } + pluginRepoFn := func() (*hostplugin.Repository, error) { + return hostplugin.NewRepository(ctx, rw, rw, kms, sche, plgm) + } + staticRepoFn := func() (*static.Repository, error) { + return static.NewRepository(ctx, rw, rw, kms) + } + staticRepo, err := staticRepoFn() + require.NoError(t, err) + at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) + r := iam.TestRole(t, conn, proj.GetPublicId()) + _ = iam.TestUserRole(t, conn, r.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, r.GetPublicId(), "id=*;type=*;actions=*") + phc := hostplugin.TestCatalogs(t, conn, proj.GetPublicId(), plg.GetPublicId(), 1)[0] + phs := hostplugin.TestSet(t, conn, kms, sche, phc, plgm) + shc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] + shs := static.TestSets(t, conn, shc.GetPublicId(), 1)[0] + + var staticPbHosts []*pb.Host + staticHosts := static.TestHosts(t, conn, shc.GetPublicId(), 10) + static.TestSetMembers(t, conn, shs.GetPublicId(), staticHosts) + for _, host := range staticHosts { + staticPbHosts = append(staticPbHosts, staticHostToProto(host, proj, shs)) + } + var pluginPbHosts []*pb.Host + for i := 0; i < 10; i++ { + extId := fmt.Sprintf("ext-id-%d", i) + extName := fmt.Sprintf("ext-name-%d", i) + pluginHost := hostplugin.TestHost(t, conn, phc.GetPublicId(), extId, hostplugin.WithExternalName(extName)) + hostplugin.TestSetMembers(t, conn, phs.GetPublicId(), []*hostplugin.Host{pluginHost}) + pluginPbHosts = append(pluginPbHosts, pluginHostToProto(pluginHost, proj, phs, plg, extId, extName)) + } + + // Since we list by create_time descending, we need to reverse slices + slices.Reverse(staticPbHosts) + slices.Reverse(pluginPbHosts) + + // Run analyze to update postgres estimates + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + // Test without anon user + requestInfo := authpb.RequestInfo{ + TokenFormat: uint32(auth.AuthTokenTypeBearer), + PublicId: at.GetPublicId(), + Token: at.GetToken(), + } + requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) + ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) + + s, err := hosts.NewService(ctx, staticRepoFn, pluginRepoFn, 1000) + require.NoError(t, err) + + t.Run("static-hosts", func(t *testing.T) { + // Start paginating, recursively + req := &pbs.ListHostsRequest{ + HostCatalogId: shc.GetPublicId(), + Filter: "", + ListToken: "", + PageSize: 2, + } + got, err := s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 2) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: staticPbHosts[0:2], + ResponseType: "delta", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + + // Request second page + req.ListToken = got.ListToken + got, err = s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 2) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: staticPbHosts[2:4], + ResponseType: "delta", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + + // Request rest of results + req.ListToken = got.ListToken + req.PageSize = 10 + got, err = s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 6) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: staticPbHosts[4:], + ResponseType: "complete", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + + // Create another Host + staticHost := static.TestHosts(t, conn, shc.GetPublicId(), 1)[0] + static.TestSetMembers(t, conn, shs.GetPublicId(), []*static.Host{staticHost}) + // Add to the front since it's most recently updated + staticPbHosts = append([]*pb.Host{staticHostToProto(staticHost, proj, shs)}, staticPbHosts...) + + // Delete one of the other Hosts + _, err = staticRepo.DeleteHost(ctx, proj.GetPublicId(), staticPbHosts[len(staticPbHosts)-1].Id) + require.NoError(t, err) + deletedHost := staticPbHosts[len(staticPbHosts)-1] + staticPbHosts = staticPbHosts[:len(staticPbHosts)-1] + + // Update one of the other Hosts + staticPbHosts[1].Name = wrapperspb.String("new-name") + staticPbHosts[1].Version = 2 + h := &static.Host{ + Host: &store.Host{ + PublicId: staticPbHosts[1].Id, + CatalogId: staticPbHosts[1].HostCatalogId, + Name: staticPbHosts[1].Name.GetValue(), + }, + } + newHost, _, err := staticRepo.UpdateHost(ctx, proj.PublicId, h, 1, []string{"name"}) + require.NoError(t, err) + staticPbHosts[1].UpdatedTime = newHost.GetUpdateTime().GetTimestamp() + staticPbHosts[1].Version = newHost.GetVersion() + // Add to the front since it's most recently updated + staticPbHosts = append( + []*pb.Host{staticPbHosts[1]}, + append( + []*pb.Host{staticPbHosts[0]}, + staticPbHosts[2:]..., + )..., + ) + // Run analyze to update postgres estimates + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + // Request updated results + req.ListToken = got.ListToken + req.PageSize = 1 + got, err = s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: []*pb.Host{staticPbHosts[0]}, + ResponseType: "delta", + ListToken: "", + SortBy: "updated_time", + SortDir: "desc", + // Should contain the deleted Host + RemovedIds: []string{deletedHost.Id}, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + + // Get next page + req.ListToken = got.ListToken + got, err = s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: []*pb.Host{staticPbHosts[1]}, + ResponseType: "complete", + ListToken: "", + SortBy: "updated_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + + // Request new page with filter requiring looping + // to fill the page. + req.ListToken = "" + req.PageSize = 1 + req.Filter = fmt.Sprintf(`"/item/id"==%q or "/item/id"==%q`, staticPbHosts[len(staticPbHosts)-2].Id, staticPbHosts[len(staticPbHosts)-1].Id) + got, err = s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: []*pb.Host{staticPbHosts[len(staticPbHosts)-2]}, + ResponseType: "delta", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + // Should be empty again + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + + // Get the second page + req.ListToken = got.ListToken + got, err = s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: []*pb.Host{staticPbHosts[len(staticPbHosts)-1]}, + ResponseType: "complete", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + }) + + t.Run("plugin-hosts", func(t *testing.T) { + // Start paginating, recursively + req := &pbs.ListHostsRequest{ + HostCatalogId: phc.GetPublicId(), + Filter: "", + ListToken: "", + PageSize: 2, + } + got, err := s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 2) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: pluginPbHosts[0:2], + ResponseType: "delta", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + + // Request second page + req.ListToken = got.ListToken + got, err = s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 2) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: pluginPbHosts[2:4], + ResponseType: "delta", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + + // Request rest of results + req.ListToken = got.ListToken + req.PageSize = 10 + got, err = s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 6) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: pluginPbHosts[4:], + ResponseType: "complete", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + + // Create another Host + extId := "ext-id-10" + extName := "ext-name-10" + pluginHost := hostplugin.TestHost(t, conn, phc.GetPublicId(), extId, hostplugin.WithExternalName(extName)) + hostplugin.TestSetMembers(t, conn, phs.GetPublicId(), []*hostplugin.Host{pluginHost}) + // Add to the front since it's most recently updated + pluginPbHosts = append([]*pb.Host{pluginHostToProto(pluginHost, proj, phs, plg, extId, extName)}, pluginPbHosts...) + + // Note: it's non-trivial to delete and update plugin hosts, so we skip that part here. + + // Run analyze to update postgres estimates + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + // Request updated results + req.ListToken = got.ListToken + got, err = s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: []*pb.Host{pluginPbHosts[0]}, + ResponseType: "complete", + ListToken: "", + SortBy: "updated_time", + SortDir: "desc", + EstItemCount: 11, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + + // Request new page with filter requiring looping + // to fill the page. + req.ListToken = "" + req.PageSize = 1 + req.Filter = fmt.Sprintf(`"/item/id"==%q or "/item/id"==%q`, pluginPbHosts[len(pluginPbHosts)-2].Id, pluginPbHosts[len(pluginPbHosts)-1].Id) + got, err = s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: []*pb.Host{pluginPbHosts[len(pluginPbHosts)-2]}, + ResponseType: "delta", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 11, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + req.ListToken = got.ListToken + // Get the second page + got, err = s.ListHosts(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostsResponse{ + Items: []*pb.Host{pluginPbHosts[len(pluginPbHosts)-1]}, + ResponseType: "complete", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 11, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostsResponse{}, "list_token"), + ), + ) + }) +} + func TestDelete(t *testing.T) { t.Parallel() ctx := context.Background() @@ -559,7 +1076,7 @@ func TestDelete(t *testing.T) { pluginHc := hostplugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) pluginH := hostplugin.TestHost(t, conn, pluginHc.GetPublicId(), "test") - s, err := hosts.NewService(ctx, repoFn, pluginRepoFn) + s, err := hosts.NewService(ctx, repoFn, pluginRepoFn, 1000) require.NoError(t, err, "Couldn't create a new host set service.") cases := []struct { @@ -647,7 +1164,7 @@ func TestDelete_twice(t *testing.T) { hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] - s, err := hosts.NewService(testCtx, repoFn, pluginRepoFn) + s, err := hosts.NewService(testCtx, repoFn, pluginRepoFn, 1000) require.NoError(err, "Couldn't create a new host set service.") req := &pbs.DeleteHostRequest{ Id: h.GetPublicId(), @@ -859,7 +1376,7 @@ func TestCreate(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := hosts.NewService(ctx, repoFn, pluginRepoFn) + s, err := hosts.NewService(ctx, repoFn, pluginRepoFn, 1000) require.NoError(err, "Failed to create a new host set service.") got, gErr := s.CreateHost(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req) @@ -949,7 +1466,7 @@ func TestUpdate_Static(t *testing.T) { hCreated := h.GetCreateTime().GetTimestamp().AsTime() - tested, err := hosts.NewService(ctx, repoFn, pluginRepoFn) + tested, err := hosts.NewService(ctx, repoFn, pluginRepoFn, 1000) require.NoError(t, err, "Failed to create a new host set service.") cases := []struct { @@ -1406,7 +1923,7 @@ func TestUpdate_Plugin(t *testing.T) { hc := hostplugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) h := hostplugin.TestHost(t, conn, hc.GetPublicId(), "test") - tested, err := hosts.NewService(ctx, repoFn, pluginRepoFn) + tested, err := hosts.NewService(ctx, repoFn, pluginRepoFn, 1000) require.NoError(t, err) got, err := tested.UpdateHost(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), &pbs.UpdateHostRequest{ diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index 30bb90e217..9c2d19cae6 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -1884,6 +1884,21 @@ "in": "query", "required": false, "type": "string" + }, + { + "name": "list_token", + "description": "An opaque token used to continue an existing iteration or\nrequest updated items. If not specified, pagination\nwill start from the beginning.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "page_size", + "description": "The maximum size of a page in this iteration.\nIf unset, the default page size configured will be used.\nIf the page_size is greater than the default page configured,\nan error will be returned.", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" } ], "tags": [ @@ -7795,6 +7810,34 @@ "type": "object", "$ref": "#/definitions/controller.api.resources.hosts.v1.Host" } + }, + "response_type": { + "type": "string", + "description": "The type of response, either \"delta\" or \"complete\".\nDelta signifies that this is part of a paginated result\nor an update to a previously completed pagination.\nComplete signifies that it is the last page." + }, + "list_token": { + "type": "string", + "description": "An opaque token used to continue an existing pagination or\nrequest updated items. Use this token in the next list request\nto request the next page." + }, + "sort_by": { + "type": "string", + "description": "The name of the field which the items are sorted by." + }, + "sort_dir": { + "type": "string", + "description": "The direction of the sort, either \"asc\" or \"desc\"." + }, + "removed_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of item IDs that have been removed since they were returned\nas part of a pagination. They should be dropped from any client cache.\nThis may contain items that are not known to the cache, if they were\ncreated and deleted between listings." + }, + "est_item_count": { + "type": "integer", + "format": "int64", + "description": "An estimate at the total items available. This may change during pagination." } } }, diff --git a/internal/gen/controller/api/services/host_service.pb.go b/internal/gen/controller/api/services/host_service.pb.go index a93b06fcdd..82ca0335ff 100644 --- a/internal/gen/controller/api/services/host_service.pb.go +++ b/internal/gen/controller/api/services/host_service.pb.go @@ -129,6 +129,15 @@ type ListHostsRequest struct { HostCatalogId string `protobuf:"bytes,1,opt,name=host_catalog_id,proto3" json:"host_catalog_id,omitempty" class:"public" eventstream:"observation"` // @gotags: `class:"public" eventstream:"observation"` Filter string `protobuf:"bytes,30,opt,name=filter,proto3" json:"filter,omitempty" class:"public"` // @gotags: `class:"public"` + // An opaque token used to continue an existing iteration or + // request updated items. If not specified, pagination + // will start from the beginning. + ListToken string `protobuf:"bytes,40,opt,name=list_token,proto3" json:"list_token,omitempty" class:"public"` // @gotags: `class:"public"` + // The maximum size of a page in this iteration. + // If unset, the default page size configured will be used. + // If the page_size is greater than the default page configured, + // an error will be returned. + PageSize uint32 `protobuf:"varint,50,opt,name=page_size,proto3" json:"page_size,omitempty" class:"public"` // @gotags: `class:"public"` } func (x *ListHostsRequest) Reset() { @@ -177,12 +186,46 @@ func (x *ListHostsRequest) GetFilter() string { return "" } +func (x *ListHostsRequest) GetListToken() string { + if x != nil { + return x.ListToken + } + return "" +} + +func (x *ListHostsRequest) GetPageSize() uint32 { + if x != nil { + return x.PageSize + } + return 0 +} + type ListHostsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Items []*hosts.Host `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + // The type of response, either "delta" or "complete". + // Delta signifies that this is part of a paginated result + // or an update to a previously completed pagination. + // Complete signifies that it is the last page. + ResponseType string `protobuf:"bytes,2,opt,name=response_type,proto3" json:"response_type,omitempty" class:"public"` // @gotags: `class:"public"` + // An opaque token used to continue an existing pagination or + // request updated items. Use this token in the next list request + // to request the next page. + ListToken string `protobuf:"bytes,3,opt,name=list_token,proto3" json:"list_token,omitempty" class:"public"` // @gotags: `class:"public"` + // The name of the field which the items are sorted by. + SortBy string `protobuf:"bytes,4,opt,name=sort_by,proto3" json:"sort_by,omitempty" class:"public"` // @gotags: `class:"public"` + // The direction of the sort, either "asc" or "desc". + SortDir string `protobuf:"bytes,5,opt,name=sort_dir,proto3" json:"sort_dir,omitempty" class:"public"` // @gotags: `class:"public"` + // A list of item IDs that have been removed since they were returned + // as part of a pagination. They should be dropped from any client cache. + // This may contain items that are not known to the cache, if they were + // created and deleted between listings. + RemovedIds []string `protobuf:"bytes,6,rep,name=removed_ids,proto3" json:"removed_ids,omitempty" class:"public"` // @gotags: `class:"public"` + // An estimate at the total items available. This may change during pagination. + EstItemCount uint32 `protobuf:"varint,7,opt,name=est_item_count,proto3" json:"est_item_count,omitempty" class:"public"` // @gotags: `class:"public"` } func (x *ListHostsResponse) Reset() { @@ -224,6 +267,48 @@ func (x *ListHostsResponse) GetItems() []*hosts.Host { return nil } +func (x *ListHostsResponse) GetResponseType() string { + if x != nil { + return x.ResponseType + } + return "" +} + +func (x *ListHostsResponse) GetListToken() string { + if x != nil { + return x.ListToken + } + return "" +} + +func (x *ListHostsResponse) GetSortBy() string { + if x != nil { + return x.SortBy + } + return "" +} + +func (x *ListHostsResponse) GetSortDir() string { + if x != nil { + return x.SortDir + } + return "" +} + +func (x *ListHostsResponse) GetRemovedIds() []string { + if x != nil { + return x.RemovedIds + } + return nil +} + +func (x *ListHostsResponse) GetEstItemCount() uint32 { + if x != nil { + return x.EstItemCount + } + return 0 +} + type CreateHostRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -548,106 +633,122 @@ var file_controller_api_services_v1_host_service_proto_rawDesc = []byte{ 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, - 0x74, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x54, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x48, - 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x0f, 0x68, - 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x61, 0x74, 0x61, 0x6c, - 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, - 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x52, 0x0a, + 0x74, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x92, 0x01, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, + 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x0f, + 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x61, 0x74, 0x61, + 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x1e, + 0x0a, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x28, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1c, + 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x32, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x98, 0x02, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, - 0x73, 0x22, 0x50, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, - 0x68, 0x6f, 0x73, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x04, 0x69, - 0x74, 0x65, 0x6d, 0x22, 0x63, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x04, 0x69, - 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x2e, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, - 0x73, 0x74, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x9e, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x3b, - 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x3c, 0x0a, 0x0b, 0x75, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x0b, 0x75, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x22, 0x51, 0x0a, 0x12, 0x55, 0x70, 0x64, + 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x69, 0x73, + 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, + 0x62, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x62, + 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x64, 0x69, 0x72, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x64, 0x69, 0x72, 0x12, 0x20, 0x0a, + 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x5f, 0x69, 0x64, 0x73, 0x12, + 0x26, 0x0a, 0x0e, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x74, 0x65, + 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x50, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x04, + 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, + 0x6f, 0x73, 0x74, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x63, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x23, 0x0a, 0x11, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, - 0x64, 0x22, 0x14, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xb9, 0x06, 0x0a, 0x0b, 0x48, 0x6f, 0x73, 0x74, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x98, 0x01, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x48, - 0x6f, 0x73, 0x74, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, - 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x34, 0x92, 0x41, - 0x15, 0x12, 0x13, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, - 0x20, 0x48, 0x6f, 0x73, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x62, 0x04, 0x69, 0x74, - 0x65, 0x6d, 0x12, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x2f, 0x7b, 0x69, - 0x64, 0x7d, 0x12, 0xa9, 0x01, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, - 0x12, 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, - 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3f, 0x92, - 0x41, 0x2b, 0x12, 0x29, 0x4c, 0x69, 0x73, 0x74, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x48, 0x6f, 0x73, - 0x74, 0x73, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, - 0x66, 0x69, 0x65, 0x64, 0x20, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x2e, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x0b, 0x12, 0x09, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x12, 0xa4, - 0x01, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x2d, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x37, 0x92, 0x41, - 0x17, 0x12, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, - 0x6c, 0x65, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x04, - 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x09, 0x2f, 0x76, 0x31, 0x2f, - 0x68, 0x6f, 0x73, 0x74, 0x73, 0x12, 0xa2, 0x01, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x48, 0x6f, 0x73, 0x74, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, + 0x69, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x68, 0x6f, 0x73, 0x74, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x9e, + 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x68, 0x6f, + 0x73, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x04, 0x69, 0x74, 0x65, + 0x6d, 0x12, 0x3c, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, + 0x73, 0x6b, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x22, + 0x51, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x68, + 0x6f, 0x73, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x04, 0x69, 0x74, + 0x65, 0x6d, 0x22, 0x23, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x14, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xb9, 0x06, + 0x0a, 0x0b, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x98, 0x01, + 0x0a, 0x07, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x34, 0x92, 0x41, 0x15, 0x12, 0x13, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, + 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x16, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, + 0x73, 0x74, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xa9, 0x01, 0x0a, 0x09, 0x4c, 0x69, 0x73, + 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x3f, 0x92, 0x41, 0x2b, 0x12, 0x29, 0x4c, 0x69, 0x73, 0x74, 0x20, 0x61, + 0x6c, 0x6c, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x43, 0x61, 0x74, 0x61, 0x6c, + 0x6f, 0x67, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0b, 0x12, 0x09, 0x2f, 0x76, 0x31, 0x2f, 0x68, + 0x6f, 0x73, 0x74, 0x73, 0x12, 0xa4, 0x01, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, + 0x6f, 0x73, 0x74, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x35, 0x92, 0x41, 0x10, 0x12, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x20, 0x61, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x3a, 0x04, - 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x32, 0x0e, 0x2f, 0x76, 0x31, 0x2f, - 0x68, 0x6f, 0x73, 0x74, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x96, 0x01, 0x0a, 0x0a, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x37, 0x92, 0x41, 0x17, 0x12, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x20, + 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x2e, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, + 0x22, 0x09, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x12, 0xa2, 0x01, 0x0a, 0x0a, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, + 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x6f, 0x73, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0x92, 0x41, 0x10, 0x12, 0x0e, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x20, 0x61, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x2e, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x10, 0x2a, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x2f, 0x7b, - 0x69, 0x64, 0x7d, 0x42, 0x55, 0xa2, 0xe3, 0x29, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x5a, 0x4b, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0x92, 0x41, 0x10, 0x12, 0x0e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x20, 0x61, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x2e, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x1c, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, + 0x32, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, + 0x12, 0x96, 0x01, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x12, + 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, + 0x92, 0x41, 0x10, 0x12, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x20, 0x61, 0x20, 0x48, 0x6f, + 0x73, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x2a, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x68, + 0x6f, 0x73, 0x74, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x42, 0x55, 0xa2, 0xe3, 0x29, 0x04, 0x68, + 0x6f, 0x73, 0x74, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, + 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/proto/controller/api/services/v1/host_service.proto b/internal/proto/controller/api/services/v1/host_service.proto index 4605c328d0..36a24f8f33 100644 --- a/internal/proto/controller/api/services/v1/host_service.proto +++ b/internal/proto/controller/api/services/v1/host_service.proto @@ -86,10 +86,39 @@ message GetHostResponse { message ListHostsRequest { string host_catalog_id = 1 [json_name = "host_catalog_id"]; // @gotags: `class:"public" eventstream:"observation"` string filter = 30 [json_name = "filter"]; // @gotags: `class:"public"` + // An opaque token used to continue an existing iteration or + // request updated items. If not specified, pagination + // will start from the beginning. + string list_token = 40 [json_name = "list_token"]; // @gotags: `class:"public"` + // The maximum size of a page in this iteration. + // If unset, the default page size configured will be used. + // If the page_size is greater than the default page configured, + // an error will be returned. + uint32 page_size = 50 [json_name = "page_size"]; // @gotags: `class:"public"` } message ListHostsResponse { repeated api.resources.hosts.v1.Host items = 1; + // The type of response, either "delta" or "complete". + // Delta signifies that this is part of a paginated result + // or an update to a previously completed pagination. + // Complete signifies that it is the last page. + string response_type = 2 [json_name = "response_type"]; // @gotags: `class:"public"` + // An opaque token used to continue an existing pagination or + // request updated items. Use this token in the next list request + // to request the next page. + string list_token = 3 [json_name = "list_token"]; // @gotags: `class:"public"` + // The name of the field which the items are sorted by. + string sort_by = 4 [json_name = "sort_by"]; // @gotags: `class:"public"` + // The direction of the sort, either "asc" or "desc". + string sort_dir = 5 [json_name = "sort_dir"]; // @gotags: `class:"public"` + // A list of item IDs that have been removed since they were returned + // as part of a pagination. They should be dropped from any client cache. + // This may contain items that are not known to the cache, if they were + // created and deleted between listings. + repeated string removed_ids = 6 [json_name = "removed_ids"]; // @gotags: `class:"public"` + // An estimate at the total items available. This may change during pagination. + uint32 est_item_count = 7 [json_name = "est_item_count"]; // @gotags: `class:"public"` } message CreateHostRequest {