diff --git a/internal/daemon/controller/common/common.go b/internal/daemon/controller/common/common.go index 286696d089..9f4eb0a9b2 100644 --- a/internal/daemon/controller/common/common.go +++ b/internal/daemon/controller/common/common.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/internal/credential" credstatic "github.com/hashicorp/boundary/internal/credential/static" "github.com/hashicorp/boundary/internal/credential/vault" + "github.com/hashicorp/boundary/internal/host" pluginhost "github.com/hashicorp/boundary/internal/host/plugin" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" @@ -25,6 +26,7 @@ type ( VaultCredentialRepoFactory = func() (*vault.Repository, error) StaticCredentialRepoFactory = func() (*credstatic.Repository, error) CredentialStoreRepoFactory func() (*credential.StoreRepository, error) + HostCatalogRepoFactory func() (*host.CatalogRepository, error) IamRepoFactory = iam.IamRepoFactory OidcAuthRepoFactory = oidc.OidcRepoFactory LdapAuthRepoFactory = ldap.RepoFactory diff --git a/internal/daemon/controller/controller.go b/internal/daemon/controller/controller.go index 868cee984d..b885d6c831 100644 --- a/internal/daemon/controller/controller.go +++ b/internal/daemon/controller/controller.go @@ -30,6 +30,7 @@ import ( "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/event" intglobals "github.com/hashicorp/boundary/internal/globals" + "github.com/hashicorp/boundary/internal/host" pluginhost "github.com/hashicorp/boundary/internal/host/plugin" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" @@ -131,6 +132,7 @@ type Controller struct { VaultCredentialRepoFn common.VaultCredentialRepoFactory StaticCredentialRepoFn common.StaticCredentialRepoFactory CredentialStoreRepoFn common.CredentialStoreRepoFactory + HostCatalogRepoFn common.HostCatalogRepoFactory IamRepoFn common.IamRepoFactory OidcRepoFn common.OidcAuthRepoFactory LdapRepoFn common.LdapAuthRepoFactory @@ -412,6 +414,9 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { c.CredentialStoreRepoFn = func() (*credential.StoreRepository, error) { return credential.NewStoreRepository(ctx, dbase, dbase) } + c.HostCatalogRepoFn = func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(ctx, dbase, dbase) + } c.ServersRepoFn = func() (*server.Repository, error) { return server.NewRepository(ctx, dbase, dbase, c.kms) } diff --git a/internal/daemon/controller/handler.go b/internal/daemon/controller/handler.go index dc72fc2a99..c71e082e0c 100644 --- a/internal/daemon/controller/handler.go +++ b/internal/daemon/controller/handler.go @@ -136,7 +136,15 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error { currentServices := s.GetServiceInfo() if _, ok := currentServices[services.HostCatalogService_ServiceDesc.ServiceName]; !ok { - hcs, err := host_catalogs.NewService(c.baseContext, c.StaticHostRepoFn, c.PluginHostRepoFn, c.PluginRepoFn, c.IamRepoFn) + hcs, err := host_catalogs.NewService( + c.baseContext, + c.StaticHostRepoFn, + c.PluginHostRepoFn, + c.PluginRepoFn, + c.IamRepoFn, + c.HostCatalogRepoFn, + c.conf.RawConfig.Controller.MaxPageSize, + ) if err != nil { return fmt.Errorf("failed to create host catalog handler service: %w", err) } diff --git a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go index 22be14e288..93af971732 100644 --- a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go +++ b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go @@ -21,6 +21,8 @@ import ( pluginstore "github.com/hashicorp/boundary/internal/host/plugin/store" "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" "github.com/hashicorp/boundary/internal/plugin" "github.com/hashicorp/boundary/internal/requests" @@ -30,6 +32,7 @@ import ( "github.com/hashicorp/boundary/internal/types/subtypes" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/plugins" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" "github.com/mr-tron/base58" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" @@ -97,17 +100,27 @@ func init() { type Service struct { pbs.UnsafeHostCatalogServiceServer - staticRepoFn common.StaticRepoFactory - pluginHostRepoFn common.PluginHostRepoFactory - pluginRepoFn common.PluginRepoFactory - iamRepoFn common.IamRepoFactory + staticRepoFn common.StaticRepoFactory + pluginHostRepoFn common.PluginHostRepoFactory + pluginRepoFn common.PluginRepoFactory + iamRepoFn common.IamRepoFactory + hostCatalogRepoFn common.HostCatalogRepoFactory + maxPageSize uint } var _ pbs.HostCatalogServiceServer = (*Service)(nil) // NewService returns a host catalog Service which handles host catalog related requests to boundary and uses the provided // repositories for storage and retrieval. -func NewService(ctx context.Context, repoFn common.StaticRepoFactory, pluginHostRepoFn common.PluginHostRepoFactory, hostPluginRepoFn common.PluginRepoFactory, iamRepoFn common.IamRepoFactory) (Service, error) { +func NewService( + ctx context.Context, + repoFn common.StaticRepoFactory, + pluginHostRepoFn common.PluginHostRepoFactory, + hostPluginRepoFn common.PluginRepoFactory, + iamRepoFn common.IamRepoFactory, + hostCatalogRepoFn common.HostCatalogRepoFactory, + maxPageSize uint, +) (Service, error) { const op = "host_catalogs.NewService" if repoFn == nil { return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing static repository") @@ -121,12 +134,26 @@ func NewService(ctx context.Context, repoFn common.StaticRepoFactory, pluginHost if iamRepoFn == nil { return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing iam repository") } - return Service{staticRepoFn: repoFn, pluginHostRepoFn: pluginHostRepoFn, pluginRepoFn: hostPluginRepoFn, iamRepoFn: iamRepoFn}, nil + if hostCatalogRepoFn == nil { + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing host catalog repo") + } + if maxPageSize == 0 { + maxPageSize = uint(globals.DefaultMaxPageSize) + } + return Service{ + staticRepoFn: repoFn, + pluginHostRepoFn: pluginHostRepoFn, + pluginRepoFn: hostPluginRepoFn, + iamRepoFn: iamRepoFn, + hostCatalogRepoFn: hostCatalogRepoFn, + maxPageSize: maxPageSize, + }, nil } func (s Service) ListHostCatalogs(ctx context.Context, req *pbs.ListHostCatalogsRequest) (*pbs.ListHostCatalogsResponse, error) { + const op = "host_catalogs.(Service).ListHostCatalogs" if err := validateListRequest(ctx, req); err != nil { - return nil, err + return nil, errors.Wrap(ctx, err, op) } authResults := s.authResult(ctx, req.GetScopeId(), action.List) if authResults.Error != nil { @@ -145,85 +172,140 @@ func (s Service) ListHostCatalogs(ctx context.Context, req *pbs.ListHostCatalogs scopeIds, scopeInfoMap, err := scopeids.GetListingScopeIds( ctx, s.iamRepoFn, authResults, req.GetScopeId(), resource.HostCatalog, req.GetRecursive()) if err != nil { - return nil, err + return nil, errors.Wrap(ctx, err, op) } // If no scopes match, return an empty response if len(scopeIds) == 0 { return &pbs.ListHostCatalogsResponse{}, nil } - - items, pluginInfoMap, err := s.listFromRepo(ctx, scopeIds) - if err != nil { - return nil, err - } - if len(items) == 0 { - return &pbs.ListHostCatalogsResponse{}, nil + 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()) } - filter, err := handlers.NewFilter(ctx, req.GetFilter()) + var filterItemFn func(ctx context.Context, item host.Catalog, plgs map[string]*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.Catalog, plgs map[string]*plugin.Plugin) (bool, error) { + outputOpts, ok, err := newOutputOpts(ctx, item, authResults, scopeInfoMap, plgs) + if err != nil { + return false, err + } + if !ok { + return false, nil + } + pbItem, err := toProto(ctx, item, outputOpts...) + if err != nil { + return false, err + } + + // 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, pbItem) + if err != nil { + return false, err + } + return filter.Match(filterable), nil + } + default: + filterItemFn = func(ctx context.Context, item host.Catalog, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + } + repo, err := s.hostCatalogRepoFn() if err != nil { - return nil, err + return nil, errors.Wrap(ctx, err, op) } - finalItems := make([]*pb.HostCatalog, 0, len(items)) - res := perms.Resource{ - Type: resource.HostCatalog, + grantsHash, err := authResults.GrantsHash(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) } - for _, item := range items { - res.Id = item.GetPublicId() - res.ScopeId = item.GetProjectId() - authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() - if len(authorizedActions) == 0 { - continue - } - outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(authResults.UserId) - outputOpts := make([]handlers.Option, 0, 3) - outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields)) - if outputFields.Has(globals.ScopeField) { - outputOpts = append(outputOpts, handlers.WithScope(scopeInfoMap[item.GetProjectId()])) + var listResp *pagination.ListResponse[host.Catalog] + var plgs map[string]*plugin.Plugin + var sortBy string + if req.GetListToken() == "" { + sortBy = "created_time" + listResp, plgs, err = host.ListCatalogs(ctx, grantsHash, pageSize, filterItemFn, repo, scopeIds) + if err != nil { + return nil, err } - if outputFields.Has(globals.AuthorizedActionsField) { - outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } else { + + listToken, err := handlers.ParseListToken(ctx, req.GetListToken(), resource.HostCatalog, grantsHash) + if err != nil { + return nil, err } - if outputFields.Has(globals.AuthorizedCollectionActionsField) { - var subtype globals.Subtype - switch item.(type) { - case *static.HostCatalog: - subtype = static.Subtype - case *hostplugin.HostCatalog: - subtype = hostplugin.Subtype + switch st := listToken.Subtype.(type) { + case *listtoken.PaginationToken: + sortBy = "created_time" + listResp, plgs, err = host.ListCatalogsPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds) + if err != nil { + return nil, err } - if subtype != "" { - collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap[subtype], authResults.Scope.Id, item.GetPublicId()) - if err != nil { - return nil, err - } - outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + case *listtoken.StartRefreshToken: + sortBy = "updated_time" + listResp, plgs, err = host.ListCatalogsRefresh(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds) + if err != nil { + return nil, err } - } - switch hc := item.(type) { - case *hostplugin.HostCatalog: - if plgInfo, ok := pluginInfoMap[hc.GetPluginId()]; ok { - outputOpts = append(outputOpts, handlers.WithPlugin(plgInfo)) + case *listtoken.RefreshToken: + sortBy = "updated_time" + listResp, plgs, err = host.ListCatalogsRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds) + if err != nil { + return nil, err } + default: + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "unexpected list token subtype: %T", st) } + } + finalItems := make([]*pb.HostCatalog, 0, len(listResp.Items)) + for _, item := range listResp.Items { + outputOpts, ok, err := newOutputOpts(ctx, item, authResults, scopeInfoMap, plgs) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if !ok { + continue + } item, err := toProto(ctx, item, outputOpts...) if err != nil { - return nil, err + return nil, errors.Wrap(ctx, err, op) } + finalItems = append(finalItems, item) + } + respType := "delta" + if listResp.CompleteListing { + respType = "complete" + } + resp := &pbs.ListHostCatalogsResponse{ + 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_CATALOG) if err != nil { return nil, err } - if filter.Match(filterable) { - finalItems = append(finalItems, item) - } } - return &pbs.ListHostCatalogsResponse{Items: finalItems}, nil + + return resp, nil } // GetHostCatalog implements the interface pbs.HostCatalogServiceServer. @@ -450,38 +532,6 @@ func (s Service) getFromRepo(ctx context.Context, id string) (host.Catalog, *plu return cat, plg, nil } -func (s Service) listFromRepo(ctx context.Context, projectIds []string) ([]host.Catalog, map[string]*plugins.PluginInfo, error) { - repo, err := s.staticRepoFn() - if err != nil { - return nil, nil, err - } - ul, err := repo.ListCatalogs(ctx, projectIds, static.WithLimit(-1)) - if err != nil { - return nil, nil, err - } - var res []host.Catalog - for _, c := range ul { - res = append(res, c) - } - pluginRepo, err := s.pluginHostRepoFn() - if err != nil { - return nil, nil, err - } - pl, plgs, err := pluginRepo.ListCatalogs(ctx, projectIds, host.WithLimit(-1)) - if err != nil { - return nil, nil, err - } - for _, c := range pl { - res = append(res, c) - } - pluginsMap := make(map[string]*plugins.PluginInfo, len(plgs)) - for _, plg := range plgs { - pluginsMap[plg.GetPublicId()] = toPluginInfo(plg) - } - - return res, pluginsMap, nil -} - func (s Service) createStaticInRepo(ctx context.Context, projId string, item *pb.HostCatalog) (*static.HostCatalog, error) { const op = "host_catalogs.(Service).createStaticInRepo" h, err := toStorageStaticCatalog(ctx, projId, item) @@ -714,6 +764,59 @@ func toPluginInfo(plg *plugin.Plugin) *plugins.PluginInfo { } } +func newOutputOpts( + ctx context.Context, + item host.Catalog, + authResults auth.VerifyResults, + scopeInfoMap map[string]*scopes.ScopeInfo, + pluginMap map[string]*plugin.Plugin, +) ([]handlers.Option, bool, error) { + res := perms.Resource{ + Type: resource.HostCatalog, + Id: item.GetPublicId(), + ScopeId: item.GetProjectId(), + } + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { + return nil, false, nil + } + + outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(authResults.UserId) + outputOpts := make([]handlers.Option, 0, 3) + outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields)) + if outputFields.Has(globals.ScopeField) { + outputOpts = append(outputOpts, handlers.WithScope(scopeInfoMap[item.GetProjectId()])) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + var subtype globals.Subtype + switch item.(type) { + case *static.HostCatalog: + subtype = static.Subtype + case *hostplugin.HostCatalog: + subtype = hostplugin.Subtype + } + if subtype != "" { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap[subtype], authResults.Scope.Id, item.GetPublicId()) + if err != nil { + return nil, false, err + } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + } + if pluginMap != nil { + if hc, ok := item.(*hostplugin.HostCatalog); ok { + if plg, ok := pluginMap[hc.GetPluginId()]; ok { + outputOpts = append(outputOpts, handlers.WithPlugin(toPluginInfo(plg))) + } + } + } + + return outputOpts, true, nil +} + func toProto(ctx context.Context, in host.Catalog, opt ...handlers.Option) (*pb.HostCatalog, error) { const op = "host_catalog_service.toProto" opts := handlers.GetOpts(opt...) diff --git a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go index 9500a4df9f..d9a9adf826 100644 --- a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go +++ b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go @@ -7,7 +7,7 @@ import ( "context" "errors" "fmt" - "sort" + "slices" "strings" "testing" @@ -15,18 +15,25 @@ 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/host_catalogs" "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" + pstore "github.com/hashicorp/boundary/internal/host/plugin/store" "github.com/hashicorp/boundary/internal/host/static" + sstore "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/hostcatalogs" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/plugins" @@ -77,6 +84,41 @@ var authorizedCollectionActions = map[globals.Subtype]map[string]*structpb.ListV var testAuthorizedActions = []string{"no-op", "read", "update", "delete"} +func pluginCatalogToProto(hc *hostplugin.HostCatalog, plg *plugin.Plugin, project *iam.Scope) *pb.HostCatalog { + return &pb.HostCatalog{ + Id: hc.GetPublicId(), + ScopeId: hc.GetProjectId(), + CreatedTime: hc.GetCreateTime().GetTimestamp(), + UpdatedTime: hc.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: project.GetPublicId(), Type: scope.Project.String(), ParentScopeId: project.GetParentId()}, + PluginId: plg.GetPublicId(), + Plugin: &plugins.PluginInfo{ + Id: plg.GetPublicId(), + Name: plg.GetName(), + Description: plg.GetDescription(), + }, + Version: 1, + Type: hostplugin.Subtype.String(), + SecretsHmac: base58.Encode(hc.SecretsHmac), + AuthorizedActions: testAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions[hostplugin.Subtype], + } +} + +func staticCatalogToProto(hc *static.HostCatalog, project *iam.Scope) *pb.HostCatalog { + return &pb.HostCatalog{ + Id: hc.GetPublicId(), + ScopeId: hc.GetProjectId(), + Scope: &scopepb.ScopeInfo{Id: project.GetPublicId(), Type: scope.Project.String(), ParentScopeId: project.GetParentId()}, + CreatedTime: hc.CreateTime.GetTimestamp(), + UpdatedTime: hc.UpdateTime.GetTimestamp(), + Version: 1, + Type: "static", + AuthorizedActions: testAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions[static.Subtype], + } +} + func TestGet_Static(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") @@ -86,34 +128,28 @@ func TestGet_Static(t *testing.T) { _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) rw := db.New(conn) - repo := func() (*static.Repository, error) { + staticRepoFn := func() (*static.Repository, error) { return static.NewRepository(ctx, rw, rw, kms) } - pluginHostRepo := func() (*hostplugin.Repository, error) { + pluginHostRepoFn := func() (*hostplugin.Repository, error) { return hostplugin.NewRepository(ctx, rw, rw, kms, sche, map[string]plgpb.HostPluginServiceClient{}) } - pluginRepo := func() (*plugin.Repository, error) { + pluginRepoFn := func() (*plugin.Repository, error) { return plugin.NewRepository(ctx, rw, rw, kms) } iamRepoFn := func() (*iam.Repository, error) { return iam.TestRepo(t, conn, wrapper), nil } + catalogServiceFn := func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(ctx, rw, rw) + } hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] toMerge := &pbs.GetHostCatalogRequest{ Id: hc.GetPublicId(), } - pHostCatalog := &pb.HostCatalog{ - Id: hc.GetPublicId(), - ScopeId: hc.GetProjectId(), - Scope: &scopepb.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: proj.GetParentId()}, - CreatedTime: hc.CreateTime.GetTimestamp(), - UpdatedTime: hc.UpdateTime.GetTimestamp(), - Type: "static", - AuthorizedActions: testAuthorizedActions, - AuthorizedCollectionActions: authorizedCollectionActions[static.Subtype], - } + pHostCatalog := staticCatalogToProto(hc, proj) cases := []struct { name string @@ -151,7 +187,7 @@ func TestGet_Static(t *testing.T) { req := proto.Clone(toMerge).(*pbs.GetHostCatalogRequest) proto.Merge(req, tc.req) - s, err := host_catalogs.NewService(ctx, repo, pluginHostRepo, pluginRepo, iamRepoFn) + s, err := host_catalogs.NewService(ctx, staticRepoFn, pluginHostRepoFn, pluginRepoFn, iamRepoFn, catalogServiceFn, 1000) require.NoError(err, "Couldn't create a new host catalog service.") got, gErr := s.GetHostCatalog(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), req) @@ -199,6 +235,9 @@ func TestGet_Plugin(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.TestRepo(t, conn, wrapper), nil } + catalogServiceFn := func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(ctx, rw, rw) + } name := "test" plg := plugin.TestPlugin(t, conn, name) hc := hostplugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId(), hostplugin.WithSecretsHmac([]byte("foobar"))) @@ -208,27 +247,7 @@ func TestGet_Plugin(t *testing.T) { Id: hc.GetPublicId(), } - pHostCatalog := &pb.HostCatalog{ - Id: hc.GetPublicId(), - ScopeId: hc.GetProjectId(), - Scope: &scopepb.ScopeInfo{ - Id: proj.GetPublicId(), - Type: scope.Project.String(), - ParentScopeId: proj.GetParentId(), - }, - PluginId: plg.GetPublicId(), - Plugin: &plugins.PluginInfo{ - Id: plg.GetPublicId(), - Name: plg.GetName(), - Description: plg.GetDescription(), - }, - CreatedTime: hc.CreateTime.GetTimestamp(), - UpdatedTime: hc.UpdateTime.GetTimestamp(), - Type: hostplugin.Subtype.String(), - AuthorizedActions: testAuthorizedActions, - AuthorizedCollectionActions: authorizedCollectionActions[hostplugin.Subtype], - SecretsHmac: base58.Encode([]byte("foobar")), - } + pHostCatalog := pluginCatalogToProto(hc, plg, proj) cases := []struct { name string @@ -277,7 +296,7 @@ func TestGet_Plugin(t *testing.T) { req := proto.Clone(toMerge).(*pbs.GetHostCatalogRequest) proto.Merge(req, tc.req) - s, err := host_catalogs.NewService(ctx, repo, pluginHostRepo, pluginRepo, iamRepoFn) + s, err := host_catalogs.NewService(ctx, repo, pluginHostRepo, pluginRepo, iamRepoFn, catalogServiceFn, 1000) require.NoError(err, "Couldn't create a new host catalog service.") got, gErr := s.GetHostCatalog(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), req) @@ -324,24 +343,17 @@ func TestList(t *testing.T) { return static.NewRepository(ctx, rw, rw, kms) } iamRepo := iam.TestRepo(t, conn, wrapper) + catalogServiceFn := func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(ctx, rw, rw) + } _, pNoCatalogs := iam.TestScopes(t, iamRepo) - oWithCatalogs, pWithCatalogs := iam.TestScopes(t, iamRepo) - oWithOtherCatalogs, pWithOtherCatalogs := iam.TestScopes(t, iamRepo) + _, pWithCatalogs := iam.TestScopes(t, iamRepo) + _, pWithOtherCatalogs := iam.TestScopes(t, iamRepo) var wantSomeCatalogs []*pb.HostCatalog for _, hc := range static.TestCatalogs(t, conn, pWithCatalogs.GetPublicId(), 3) { - wantSomeCatalogs = append(wantSomeCatalogs, &pb.HostCatalog{ - Id: hc.GetPublicId(), - ScopeId: hc.GetProjectId(), - CreatedTime: hc.GetCreateTime().GetTimestamp(), - UpdatedTime: hc.GetUpdateTime().GetTimestamp(), - Scope: &scopepb.ScopeInfo{Id: pWithCatalogs.GetPublicId(), Type: scope.Project.String(), ParentScopeId: oWithCatalogs.GetPublicId()}, - Version: 1, - Type: "static", - AuthorizedActions: testAuthorizedActions, - AuthorizedCollectionActions: authorizedCollectionActions[static.Subtype], - }) + wantSomeCatalogs = append(wantSomeCatalogs, staticCatalogToProto(hc, pWithCatalogs)) } var testPluginCatalogs []*pb.HostCatalog @@ -349,63 +361,20 @@ func TestList(t *testing.T) { plg := plugin.TestPlugin(t, conn, name) for i := 0; i < 3; i++ { hc := hostplugin.TestCatalog(t, conn, pWithCatalogs.GetPublicId(), plg.GetPublicId()) - cat := &pb.HostCatalog{ - Id: hc.GetPublicId(), - ScopeId: hc.GetProjectId(), - CreatedTime: hc.GetCreateTime().GetTimestamp(), - UpdatedTime: hc.GetUpdateTime().GetTimestamp(), - Scope: &scopepb.ScopeInfo{Id: pWithCatalogs.GetPublicId(), Type: scope.Project.String(), ParentScopeId: oWithCatalogs.GetPublicId()}, - PluginId: plg.GetPublicId(), - Plugin: &plugins.PluginInfo{ - Id: plg.GetPublicId(), - Name: plg.GetName(), - Description: plg.GetDescription(), - }, - Version: 1, - Type: hostplugin.Subtype.String(), - AuthorizedActions: testAuthorizedActions, - AuthorizedCollectionActions: authorizedCollectionActions[hostplugin.Subtype], - } + cat := pluginCatalogToProto(hc, plg, pWithCatalogs) wantSomeCatalogs = append(wantSomeCatalogs, cat) testPluginCatalogs = append(testPluginCatalogs, cat) } var wantOtherCatalogs []*pb.HostCatalog for _, hc := range static.TestCatalogs(t, conn, pWithOtherCatalogs.GetPublicId(), 3) { - wantOtherCatalogs = append(wantOtherCatalogs, &pb.HostCatalog{ - Id: hc.GetPublicId(), - ScopeId: hc.GetProjectId(), - CreatedTime: hc.GetCreateTime().GetTimestamp(), - UpdatedTime: hc.GetUpdateTime().GetTimestamp(), - Scope: &scopepb.ScopeInfo{Id: pWithOtherCatalogs.GetPublicId(), Type: scope.Project.String(), ParentScopeId: oWithOtherCatalogs.GetPublicId()}, - Version: 1, - Type: "static", - AuthorizedActions: testAuthorizedActions, - AuthorizedCollectionActions: authorizedCollectionActions[static.Subtype], - }) + wantOtherCatalogs = append(wantOtherCatalogs, staticCatalogToProto(hc, pWithOtherCatalogs)) } name = "different" diffPlg := plugin.TestPlugin(t, conn, name) - for i := 0; i < 3; i++ { - hc := hostplugin.TestCatalog(t, conn, pWithOtherCatalogs.GetPublicId(), diffPlg.GetPublicId()) - wantOtherCatalogs = append(wantOtherCatalogs, &pb.HostCatalog{ - Id: hc.GetPublicId(), - ScopeId: hc.GetProjectId(), - CreatedTime: hc.GetCreateTime().GetTimestamp(), - UpdatedTime: hc.GetUpdateTime().GetTimestamp(), - Scope: &scopepb.ScopeInfo{Id: pWithOtherCatalogs.GetPublicId(), Type: scope.Project.String(), ParentScopeId: oWithOtherCatalogs.GetPublicId()}, - PluginId: diffPlg.GetPublicId(), - Plugin: &plugins.PluginInfo{ - Id: diffPlg.GetPublicId(), - Name: diffPlg.GetName(), - Description: diffPlg.GetDescription(), - }, - Version: 1, - Type: hostplugin.Subtype.String(), - AuthorizedActions: testAuthorizedActions, - AuthorizedCollectionActions: authorizedCollectionActions[hostplugin.Subtype], - }) + for _, hc := range hostplugin.TestCatalogs(t, conn, pWithOtherCatalogs.GetPublicId(), diffPlg.GetPublicId(), 3) { + wantOtherCatalogs = append(wantOtherCatalogs, pluginCatalogToProto(hc, diffPlg, pWithOtherCatalogs)) } cases := []struct { @@ -417,17 +386,34 @@ func TestList(t *testing.T) { { name: "List Some Catalogs", req: &pbs.ListHostCatalogsRequest{ScopeId: pWithCatalogs.GetPublicId()}, - res: &pbs.ListHostCatalogsResponse{Items: wantSomeCatalogs}, + res: &pbs.ListHostCatalogsResponse{ + Items: wantSomeCatalogs, + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + EstItemCount: 6, + }, }, { name: "List Other Catalogs", req: &pbs.ListHostCatalogsRequest{ScopeId: pWithOtherCatalogs.GetPublicId()}, - res: &pbs.ListHostCatalogsResponse{Items: wantOtherCatalogs}, + res: &pbs.ListHostCatalogsResponse{ + Items: wantOtherCatalogs, + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + EstItemCount: 6, + }, }, { name: "List No Catalogs", req: &pbs.ListHostCatalogsRequest{ScopeId: pNoCatalogs.GetPublicId()}, - res: &pbs.ListHostCatalogsResponse{}, + res: &pbs.ListHostCatalogsResponse{ + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + EstItemCount: 0, + }, }, { name: "Unfound Catalogs", @@ -443,7 +429,11 @@ func TestList(t *testing.T) { name: "List recursively", req: &pbs.ListHostCatalogsRequest{ScopeId: scope.Global.String(), Recursive: true}, res: &pbs.ListHostCatalogsResponse{ - Items: append(wantSomeCatalogs, wantOtherCatalogs...), + Items: append(wantSomeCatalogs, wantOtherCatalogs...), + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + EstItemCount: 12, }, }, { @@ -452,7 +442,13 @@ func TestList(t *testing.T) { ScopeId: scope.Global.String(), Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, pWithCatalogs.GetPublicId()), }, - res: &pbs.ListHostCatalogsResponse{Items: wantSomeCatalogs}, + res: &pbs.ListHostCatalogsResponse{ + Items: wantSomeCatalogs, + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + EstItemCount: 6, + }, }, { name: "Filter To Catalog Using Test Plugin", @@ -460,12 +456,23 @@ func TestList(t *testing.T) { ScopeId: scope.Global.String(), Recursive: true, Filter: `"/item/plugin/name"=="test"`, }, - res: &pbs.ListHostCatalogsResponse{Items: testPluginCatalogs}, + res: &pbs.ListHostCatalogsResponse{ + Items: testPluginCatalogs, + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + EstItemCount: 3, + }, }, { name: "Filter To No Catalogs", req: &pbs.ListHostCatalogsRequest{ScopeId: pWithCatalogs.GetPublicId(), Filter: `"/item/id"=="doesnt match"`}, - res: &pbs.ListHostCatalogsResponse{}, + res: &pbs.ListHostCatalogsResponse{ + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + EstItemCount: 0, + }, }, { name: "Filter Bad Format", @@ -476,7 +483,7 @@ func TestList(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 := host_catalogs.NewService(ctx, repoFn, pluginHostRepo, pluginRepo, iamRepoFn) + s, err := host_catalogs.NewService(ctx, repoFn, pluginHostRepo, pluginRepo, iamRepoFn, catalogServiceFn, 1000) require.NoError(err, "Couldn't create new auth_method service.") // Test with non-anon user @@ -486,12 +493,6 @@ func TestList(t *testing.T) { assert.True(errors.Is(gErr, tc.err), "ListHostCatalogs() for scope %q got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err) return } - sort.Slice(got.Items, func(i, j int) bool { - return got.Items[i].GetId() < got.Items[j].GetId() - }) - sort.Slice(tc.res.Items, func(i, j int) bool { - return tc.res.Items[i].GetId() < tc.res.Items[j].GetId() - }) assert.Empty(cmp.Diff( got, @@ -503,7 +504,8 @@ func TestList(t *testing.T) { cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), - ), "ListHostCatalogs() for scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) + protocmp.IgnoreFields(&pbs.ListHostCatalogsResponse{}, "list_token"), + )) // Test with anon user got, gErr = s.ListHostCatalogs(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -518,6 +520,374 @@ func TestList(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") + sqlDB, err := conn.SqlDB(ctx) + require.NoError(t, err) + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + plg1 := plugin.TestPlugin(t, conn, "testplugin1") + plg2 := plugin.TestPlugin(t, conn, "testplugin2") + lp, err := loopback.NewLoopbackPlugin() + require.NoError(t, err) + plgm := map[string]plgpb.HostPluginServiceClient{ + plg1.GetPublicId(): loopback.NewWrappingPluginHostClient(lp), + plg2.GetPublicId(): loopback.NewWrappingPluginHostClient(lp), + } + iamRepoFn := func() (*iam.Repository, error) { + return iam.TestRepo(t, conn, wrapper), nil + } + pluginHostRepoFn := func() (*hostplugin.Repository, error) { + return hostplugin.NewRepository(ctx, rw, rw, kms, sche, plgm) + } + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(ctx, rw, rw, kms) + } + staticRepoFn := func() (*static.Repository, error) { + return static.NewRepository(ctx, rw, rw, kms) + } + tokenRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(ctx, rw, rw, kms) + } + serversRepoFn := func() (*server.Repository, error) { + return server.NewRepository(ctx, rw, rw, kms) + } + staticRepo, err := staticRepoFn() + require.NoError(t, err) + pluginRepo, err := pluginHostRepoFn() + require.NoError(t, err) + iamRepo := iam.TestRepo(t, conn, wrapper) + catalogServiceFn := func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(ctx, rw, rw) + } + + org, proj := iam.TestScopes(t, iamRepo) + at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) + pr := iam.TestRole(t, conn, proj.GetPublicId()) + _ = iam.TestUserRole(t, conn, pr.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, pr.GetPublicId(), "id=*;type=*;actions=*") + gr := iam.TestRole(t, conn, "global") + _ = iam.TestUserRole(t, conn, gr.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, gr.GetPublicId(), "id=*;type=*;actions=*") + s, err := host_catalogs.NewService(ctx, staticRepoFn, pluginHostRepoFn, pluginRepoFn, iamRepoFn, catalogServiceFn, 1000) + require.NoError(t, err) + + var allCatalogs []*pb.HostCatalog + for i := 0; i < 5; i++ { + plg := plg1 + if i%2 == 0 { + // Create plugin catalogs with both plugins + plg = plg2 + } + allCatalogs = append(allCatalogs, staticCatalogToProto(static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0], proj)) + allCatalogs = append(allCatalogs, pluginCatalogToProto(hostplugin.TestCatalogs(t, conn, proj.GetPublicId(), plg.GetPublicId(), 1)[0], plg, proj)) + } + // Reverse slice since we're sorting by create time descending + slices.Reverse(allCatalogs) + + // 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) + + // Start paginating, recursively + req := &pbs.ListHostCatalogsRequest{ + ScopeId: "global", + Recursive: true, + Filter: "", + ListToken: "", + PageSize: 2, + } + got, err := s.ListHostCatalogs(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.ListHostCatalogsResponse{ + Items: allCatalogs[0:2], + ResponseType: "delta", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(i, j string) bool { + return i < j + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostCatalogsResponse{}, "list_token"), + ), + ) + + // Request second page + req.ListToken = got.ListToken + got, err = s.ListHostCatalogs(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.ListHostCatalogsResponse{ + Items: allCatalogs[2:4], + ResponseType: "delta", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(i, j string) bool { + return i < j + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostCatalogsResponse{}, "list_token"), + ), + ) + + // Request rest of results + req.ListToken = got.ListToken + req.PageSize = 10 + got, err = s.ListHostCatalogs(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.ListHostCatalogsResponse{ + Items: allCatalogs[4:], + ResponseType: "complete", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(i, j string) bool { + return i < j + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostCatalogsResponse{}, "list_token"), + ), + ) + + // Create another few host catalogs + // Append to start since they are the most recently created. + newStaticCatalog := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] + newPluginCatalog := hostplugin.TestCatalog(t, conn, proj.GetPublicId(), plg1.GetPublicId()) + allCatalogs = append( + []*pb.HostCatalog{ + pluginCatalogToProto(newPluginCatalog, plg1, proj), + staticCatalogToProto(newStaticCatalog, proj), + }, + allCatalogs..., + ) + + // Delete some of the other catalogs + _, err = staticRepo.DeleteCatalog(ctx, allCatalogs[len(allCatalogs)-1].Id) + require.NoError(t, err) + deletedCatalog1 := allCatalogs[len(allCatalogs)-1] + allCatalogs = allCatalogs[:len(allCatalogs)-1] + _, err = pluginRepo.DeleteCatalog(ctx, allCatalogs[len(allCatalogs)-1].Id) + require.NoError(t, err) + deletedCatalog2 := allCatalogs[len(allCatalogs)-1] + allCatalogs = allCatalogs[:len(allCatalogs)-1] + + // Update some other catalogs + allCatalogs[len(allCatalogs)-1].Name = wrapperspb.String("new-name") + allCatalogs[len(allCatalogs)-1].Version = 2 + updatedCatalog1 := &static.HostCatalog{ + HostCatalog: &sstore.HostCatalog{ + PublicId: allCatalogs[len(allCatalogs)-1].GetId(), + Name: allCatalogs[len(allCatalogs)-1].GetName().GetValue(), + ProjectId: allCatalogs[len(allCatalogs)-1].GetScopeId(), + }, + } + cat1, _, err := staticRepo.UpdateCatalog(ctx, updatedCatalog1, 1, []string{"name"}) + require.NoError(t, err) + allCatalogs[len(allCatalogs)-1].UpdatedTime = cat1.HostCatalog.UpdateTime.GetTimestamp() + allCatalogs[len(allCatalogs)-1].Version = cat1.GetVersion() + // Add to the front since it's most recently updated + allCatalogs = append( + []*pb.HostCatalog{allCatalogs[len(allCatalogs)-1]}, + allCatalogs[:len(allCatalogs)-1]..., + ) + allCatalogs[len(allCatalogs)-1].Name = wrapperspb.String("new-name") + allCatalogs[len(allCatalogs)-1].Version = 2 + updatedCatalog2 := &hostplugin.HostCatalog{ + HostCatalog: &pstore.HostCatalog{ + PublicId: allCatalogs[len(allCatalogs)-1].GetId(), + Name: allCatalogs[len(allCatalogs)-1].GetName().GetValue(), + ProjectId: allCatalogs[len(allCatalogs)-1].GetScopeId(), + }, + } + cat2, _, _, err := pluginRepo.UpdateCatalog(ctx, updatedCatalog2, 1, []string{"name"}) + require.NoError(t, err) + allCatalogs[len(allCatalogs)-1].UpdatedTime = cat2.HostCatalog.UpdateTime.GetTimestamp() + allCatalogs[len(allCatalogs)-1].Version = cat2.GetVersion() + // Add to the front since it's most recently updated + allCatalogs = append( + []*pb.HostCatalog{allCatalogs[len(allCatalogs)-1]}, + allCatalogs[:len(allCatalogs)-1]..., + ) + + // Run analyze to update postgres estimates + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + // Request updated results + req.ListToken = got.ListToken + req.PageSize = 2 + got, err = s.ListHostCatalogs(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.ListHostCatalogsResponse{ + // The first two should be the recently updated catalogs + Items: allCatalogs[:2], + ResponseType: "delta", + ListToken: "", + SortBy: "updated_time", + SortDir: "desc", + // Should contain the deleted catalogs + RemovedIds: []string{deletedCatalog1.Id, deletedCatalog2.Id}, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(i, j string) bool { + return i < j + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostCatalogsResponse{}, "list_token"), + ), + ) + + // Get the next page + req.ListToken = got.ListToken + got, err = s.ListHostCatalogs(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.ListHostCatalogsResponse{ + // The next two should be the recently created catalogs + Items: allCatalogs[2:4], + ResponseType: "complete", + ListToken: "", + SortBy: "updated_time", + SortDir: "desc", + // Should be empty again + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(i, j string) bool { + return i < j + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostCatalogsResponse{}, "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`, allCatalogs[len(allCatalogs)-2].Id, allCatalogs[len(allCatalogs)-1].Id) + got, err = s.ListHostCatalogs(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostCatalogsResponse{ + Items: []*pb.HostCatalog{allCatalogs[len(allCatalogs)-2]}, + ResponseType: "delta", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + // Should be empty again + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(i, j string) bool { + return i < j + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostCatalogsResponse{}, "list_token"), + ), + ) + req.ListToken = got.ListToken + // Get the second page + got, err = s.ListHostCatalogs(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListHostCatalogsResponse{ + Items: []*pb.HostCatalog{allCatalogs[len(allCatalogs)-1]}, + ResponseType: "complete", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpopts.SortSlices(func(i, j string) bool { + return i < j + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListHostCatalogsResponse{}, "list_token"), + ), + ) +} + func TestDelete_Static(t *testing.T) { t.Parallel() ctx := context.Background() @@ -540,9 +910,12 @@ func TestDelete_Static(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } + catalogServiceFn := func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(ctx, rw, rw) + } hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] - s, err := host_catalogs.NewService(ctx, repo, pluginHostRepo, pluginRepo, iamRepoFn) + s, err := host_catalogs.NewService(ctx, repo, pluginHostRepo, pluginRepo, iamRepoFn, catalogServiceFn, 1000) require.NoError(t, err, "Couldn't create a new host catalog service.") cases := []struct { @@ -611,10 +984,13 @@ func TestDelete_Plugin(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } + catalogServiceFn := func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(ctx, rw, rw) + } plg := plugin.TestPlugin(t, conn, "test") hc := hostplugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) - s, err := host_catalogs.NewService(ctx, repo, pluginHostRepo, pluginRepo, iamRepoFn) + s, err := host_catalogs.NewService(ctx, repo, pluginHostRepo, pluginRepo, iamRepoFn, catalogServiceFn, 1000) require.NoError(t, err, "Couldn't create a new host catalog service.") cases := []struct { @@ -684,9 +1060,12 @@ func TestDelete_twice(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } + catalogServiceFn := func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(testCtx, rw, rw) + } hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] - s, err := host_catalogs.NewService(testCtx, repo, pluginHostRepo, pluginRepo, iamRepoFn) + s, err := host_catalogs.NewService(testCtx, repo, pluginHostRepo, pluginRepo, iamRepoFn, catalogServiceFn, 1000) require.NoError(err, "Couldn't create a new host catalog service.") req := &pbs.DeleteHostCatalogRequest{ Id: hc.GetPublicId(), @@ -721,6 +1100,9 @@ func TestCreate_Static(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } + catalogServiceFn := func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(ctx, rw, rw) + } defaultHc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] defaultHcCreated := defaultHc.GetCreateTime().GetTimestamp().AsTime() toMerge := &pbs.CreateHostCatalogRequest{} @@ -820,7 +1202,7 @@ func TestCreate_Static(t *testing.T) { req := proto.Clone(toMerge).(*pbs.CreateHostCatalogRequest) proto.Merge(req, tc.req) - s, err := host_catalogs.NewService(ctx, repo, pluginHostRepo, pluginRepo, iamRepoFn) + s, err := host_catalogs.NewService(ctx, repo, pluginHostRepo, pluginRepo, iamRepoFn, catalogServiceFn, 1000) require.NoError(err, "Failed to create a new host catalog service.") got, gErr := s.CreateHostCatalog(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), req) @@ -881,6 +1263,9 @@ func TestCreate_Plugin(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } + catalogServiceFn := func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(ctx, rw, rw) + } name := "test" plg := plugin.TestPlugin(t, conn, name) @@ -1006,7 +1391,7 @@ func TestCreate_Plugin(t *testing.T) { req := proto.Clone(toMerge).(*pbs.CreateHostCatalogRequest) proto.Merge(req, tc.req) - s, err := host_catalogs.NewService(ctx, repo, pluginHostRepo, pluginRepo, iamRepoFn) + s, err := host_catalogs.NewService(ctx, repo, pluginHostRepo, pluginRepo, iamRepoFn, catalogServiceFn, 1000) require.NoError(err, "Failed to create a new host catalog service.") got, gErr := s.CreateHostCatalog(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), req) @@ -1072,7 +1457,10 @@ func TestUpdate_Static(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } - tested, err := host_catalogs.NewService(ctx, repoFn, pluginHostRepo, pluginRepo, iamRepoFn) + catalogServiceFn := func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(ctx, rw, rw) + } + tested, err := host_catalogs.NewService(ctx, repoFn, pluginHostRepo, pluginRepo, iamRepoFn, catalogServiceFn, 1000) require.NoError(t, err, "Failed to create a new host catalog service.") hc, err := static.NewHostCatalog(ctx, proj.GetPublicId(), static.WithName("default"), static.WithDescription("default")) @@ -1346,7 +1734,7 @@ func TestUpdate_Static(t *testing.T) { name: "Cant specify Updated Time", req: &pbs.UpdateHostCatalogRequest{ UpdateMask: &field_mask.FieldMask{ - Paths: []string{"updated_time"}, + Paths: []string{"created_time"}, }, Item: &pb.HostCatalog{ UpdatedTime: timestamppb.Now(), @@ -1460,7 +1848,10 @@ func TestUpdate_Plugin(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } - tested, err := host_catalogs.NewService(testCtx, repoFn, pluginHostRepo, pluginRepo, iamRepoFn) + catalogServiceFn := func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(testCtx, rw, rw) + } + tested, err := host_catalogs.NewService(testCtx, repoFn, pluginHostRepo, pluginRepo, iamRepoFn, catalogServiceFn, 1000) require.NoError(t, err, "Failed to create a new host catalog service.") ctx := auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()) @@ -1710,7 +2101,7 @@ func TestUpdate_Plugin(t *testing.T) { }, { name: "Cant specify Updated Time", - masks: []string{"updated_time"}, + masks: []string{"created_time"}, changes: []updateFn{ clearReadOnlyFields(), func(c *pb.HostCatalog) { diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index 31777a7504..f1ffd953e9 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -1483,6 +1483,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": [ @@ -7933,6 +7948,34 @@ "type": "object", "$ref": "#/definitions/controller.api.resources.hostcatalogs.v1.HostCatalog" } + }, + "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_catalog_service.pb.go b/internal/gen/controller/api/services/host_catalog_service.pb.go index edbfebf33b..ed51a979cd 100644 --- a/internal/gen/controller/api/services/host_catalog_service.pb.go +++ b/internal/gen/controller/api/services/host_catalog_service.pb.go @@ -130,6 +130,15 @@ type ListHostCatalogsRequest struct { ScopeId string `protobuf:"bytes,1,opt,name=scope_id,proto3" json:"scope_id,omitempty" class:"public" eventstream:"observation"` // @gotags: `class:"public" eventstream:"observation"` Recursive bool `protobuf:"varint,20,opt,name=recursive,proto3" json:"recursive,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 *ListHostCatalogsRequest) Reset() { @@ -185,12 +194,46 @@ func (x *ListHostCatalogsRequest) GetFilter() string { return "" } +func (x *ListHostCatalogsRequest) GetListToken() string { + if x != nil { + return x.ListToken + } + return "" +} + +func (x *ListHostCatalogsRequest) GetPageSize() uint32 { + if x != nil { + return x.PageSize + } + return 0 +} + type ListHostCatalogsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Items []*hostcatalogs.HostCatalog `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 *ListHostCatalogsResponse) Reset() { @@ -232,6 +275,48 @@ func (x *ListHostCatalogsResponse) GetItems() []*hostcatalogs.HostCatalog { return nil } +func (x *ListHostCatalogsResponse) GetResponseType() string { + if x != nil { + return x.ResponseType + } + return "" +} + +func (x *ListHostCatalogsResponse) GetListToken() string { + if x != nil { + return x.ListToken + } + return "" +} + +func (x *ListHostCatalogsResponse) GetSortBy() string { + if x != nil { + return x.SortBy + } + return "" +} + +func (x *ListHostCatalogsResponse) GetSortDir() string { + if x != nil { + return x.SortDir + } + return "" +} + +func (x *ListHostCatalogsResponse) GetRemovedIds() []string { + if x != nil { + return x.RemovedIds + } + return nil +} + +func (x *ListHostCatalogsResponse) GetEstItemCount() uint32 { + if x != nil { + return x.EstItemCount + } + return 0 +} + type CreateHostCatalogRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -569,127 +654,143 @@ var file_controller_api_services_v1_host_catalog_service_proto_rawDesc = []byte{ 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, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, - 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x6b, 0x0a, 0x17, 0x4c, 0x69, - 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, - 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x18, 0x14, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 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, 0x67, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x48, + 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0xa9, 0x01, 0x0a, 0x17, 0x4c, + 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, + 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x18, + 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, + 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, 0xad, 0x02, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 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, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x22, 0x87, 0x01, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, - 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x49, 0x0a, - 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 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, 0x63, 0x61, 0x74, 0x61, 0x6c, - 0x6f, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, - 0x6f, 0x67, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x78, 0x0a, 0x19, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 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, 0x49, 0x0a, 0x04, 0x69, 0x74, 0x65, - 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 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, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x04, - 0x69, 0x74, 0x65, 0x6d, 0x22, 0xb2, 0x01, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, - 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x49, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x35, 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, 0x63, - 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, - 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x3b, 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, 0x0a, 0x75, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x22, 0x66, 0x0a, 0x19, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 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, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x04, 0x69, 0x74, 0x65, - 0x6d, 0x22, 0x2a, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, - 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1b, 0x0a, - 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, - 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xde, 0x07, 0x0a, 0x12, 0x48, - 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x12, 0xbd, 0x01, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, - 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x31, 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, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 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, 0x43, 0x61, 0x74, 0x61, - 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x44, 0x92, 0x41, 0x1d, - 0x12, 0x1b, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, - 0x48, 0x6f, 0x73, 0x74, 0x20, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x2e, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x1e, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x16, 0x2f, 0x76, 0x31, 0x2f, 0x68, - 0x6f, 0x73, 0x74, 0x2d, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x2f, 0x7b, 0x69, 0x64, - 0x7d, 0x12, 0xba, 0x01, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, - 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x12, 0x33, 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, 0x43, 0x61, 0x74, 0x61, - 0x6c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 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, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x3b, 0x92, 0x41, 0x1f, 0x12, 0x1d, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, 0x6c, - 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x20, 0x43, 0x61, 0x74, 0x61, - 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x76, 0x31, - 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2d, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x12, 0xc2, - 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, - 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x34, 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, 0x43, 0x61, 0x74, 0x61, - 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x6e, + 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, 0x87, 0x01, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x49, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x35, 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, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, + 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x20, + 0x0a, 0x0b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x22, 0x78, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, + 0x74, 0x61, 0x6c, 0x6f, 0x67, 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, + 0x49, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 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, 0x63, 0x61, 0x74, + 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, + 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0xb2, 0x01, 0x0a, 0x18, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x49, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 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, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x04, 0x69, 0x74, + 0x65, 0x6d, 0x12, 0x3b, 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, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x22, + 0x66, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, + 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x04, + 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 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, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, + 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, + 0x67, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x2a, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x22, 0x1b, 0x0a, 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x6f, 0x73, + 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x32, 0xde, 0x07, 0x0a, 0x12, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xbd, 0x01, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x48, + 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x31, 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, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x43, + 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 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, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x40, 0x92, 0x41, 0x18, 0x12, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, - 0x61, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x20, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x1f, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, - 0x22, 0x11, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2d, 0x63, 0x61, 0x74, 0x61, 0x6c, - 0x6f, 0x67, 0x73, 0x12, 0xc7, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, - 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x34, 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, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x65, 0x22, 0x44, 0x92, 0x41, 0x1d, 0x12, 0x1b, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, 0x73, + 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x20, 0x43, 0x61, 0x74, 0x61, 0x6c, + 0x6f, 0x67, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, + 0x16, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2d, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, + 0x67, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xba, 0x01, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, + 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x12, 0x33, 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, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x34, 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, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3b, 0x92, 0x41, 0x1f, 0x12, 0x1d, 0x47, 0x65, + 0x74, 0x73, 0x20, 0x61, 0x20, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x48, 0x6f, 0x73, + 0x74, 0x20, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x13, 0x12, 0x11, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2d, 0x63, 0x61, 0x74, 0x61, + 0x6c, 0x6f, 0x67, 0x73, 0x12, 0xc2, 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, + 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x34, 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, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x35, 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, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x40, 0x92, 0x41, 0x18, 0x12, 0x16, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x20, 0x43, 0x61, 0x74, + 0x61, 0x6c, 0x6f, 0x67, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, + 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x11, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, + 0x2d, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x12, 0xc7, 0x01, 0x0a, 0x11, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, + 0x34, 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, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x45, 0x92, 0x41, 0x18, 0x12, 0x16, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x20, 0x43, 0x61, 0x74, 0x61, - 0x6c, 0x6f, 0x67, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, - 0x04, 0x69, 0x74, 0x65, 0x6d, 0x32, 0x16, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2d, - 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xbb, 0x01, - 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, - 0x6c, 0x6f, 0x67, 0x12, 0x34, 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, 0x43, 0x61, 0x74, 0x61, 0x6c, - 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 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, 0x43, 0x61, 0x74, + 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x45, 0x92, 0x41, + 0x18, 0x12, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x48, 0x6f, 0x73, + 0x74, 0x20, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x3a, + 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x32, 0x16, 0x2f, 0x76, 0x31, + 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2d, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x73, 0x2f, 0x7b, + 0x69, 0x64, 0x7d, 0x12, 0xbb, 0x01, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x6f, + 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x34, 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, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x39, 0x92, 0x41, 0x18, 0x12, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x20, 0x61, - 0x20, 0x48, 0x6f, 0x73, 0x74, 0x20, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x18, 0x2a, 0x16, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2d, 0x63, 0x61, - 0x74, 0x61, 0x6c, 0x6f, 0x67, 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, + 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x35, 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, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x39, 0x92, 0x41, 0x18, 0x12, 0x16, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x48, 0x6f, 0x73, 0x74, 0x20, 0x43, 0x61, 0x74, 0x61, + 0x6c, 0x6f, 0x67, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x2a, 0x16, 0x2f, 0x76, 0x31, 0x2f, 0x68, + 0x6f, 0x73, 0x74, 0x2d, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 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_catalog_service.proto b/internal/proto/controller/api/services/v1/host_catalog_service.proto index b65a9b0634..ddd00462e4 100644 --- a/internal/proto/controller/api/services/v1/host_catalog_service.proto +++ b/internal/proto/controller/api/services/v1/host_catalog_service.proto @@ -85,10 +85,39 @@ message ListHostCatalogsRequest { string scope_id = 1 [json_name = "scope_id"]; // @gotags: `class:"public" eventstream:"observation"` bool recursive = 20 [json_name = "recursive"]; // @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 ListHostCatalogsResponse { repeated api.resources.hostcatalogs.v1.HostCatalog 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 CreateHostCatalogRequest {