diff --git a/frontend/pkg/frontend/frontend.go b/frontend/pkg/frontend/frontend.go index ea7becf46..0aafdc974 100644 --- a/frontend/pkg/frontend/frontend.go +++ b/frontend/pkg/frontend/frontend.go @@ -13,6 +13,7 @@ import ( "net" "net/http" "os" + "path" "strconv" "strings" "sync/atomic" @@ -175,6 +176,8 @@ func (f *Frontend) ArmResourceList(writer http.ResponseWriter, request *http.Req subscriptionID := request.PathValue(PathSegmentSubscriptionID) resourceGroupName := request.PathValue(PathSegmentResourceGroupName) + resourceName := request.PathValue(PathSegmentResourceName) + resourceTypeName := path.Base(request.URL.Path) // Even though the bulk of the list content comes from Cluster Service, // we start by querying Cosmos DB because its continuation token meets @@ -185,6 +188,13 @@ func (f *Frontend) ArmResourceList(writer http.ResponseWriter, request *http.Req if resourceGroupName != "" { prefixString += "/resourceGroups/" + resourceGroupName } + if resourceName != "" { + // This is a nested resource request. Build a resource ID for + // the parent cluster. We use this below to get the cluster's + // ResourceDocument from Cosmos DB. + prefixString += "/providers/" + api.ProviderNamespace + prefixString += "/" + api.ClusterResourceTypeName + "/" + resourceName + } prefix, err := arm.ParseResourceID(prefixString) if err != nil { f.logger.Error(err.Error()) @@ -192,17 +202,31 @@ func (f *Frontend) ArmResourceList(writer http.ResponseWriter, request *http.Req return } - documentList, continuationToken, err := f.dbClient.ListResourceDocs(ctx, prefix, &api.ClusterResourceType, pageSizeHint, continuationToken) - if err != nil { - f.logger.Error(err.Error()) - arm.WriteInternalServerError(writer) - return - } + dbIterator := f.dbClient.ListResourceDocs(ctx, prefix, pageSizeHint, continuationToken) // Build a map of cluster documents by Cluster Service cluster ID. documentMap := make(map[string]*database.ResourceDocument) - for _, doc := range documentList { - documentMap[doc.InternalID.ID()] = doc + for item := range dbIterator.Items(ctx) { + var doc database.ResourceDocument + + err = json.Unmarshal(item, &doc) + if err != nil { + f.logger.Error(err.Error()) + arm.WriteInternalServerError(writer) + return + } + + // FIXME This filtering could be made part of the query expression. It would + // require some reworking (or elimination) of the DBClient interface. + if strings.HasSuffix(strings.ToLower(doc.Key.ResourceType.Type), resourceTypeName) { + documentMap[doc.InternalID.ID()] = &doc + } + } + + err = dbIterator.GetError() + if err != nil { + f.logger.Error(err.Error()) + arm.WriteInternalServerError(writer) } // Build a Cluster Service query that looks for @@ -214,39 +238,65 @@ func (f *Frontend) ArmResourceList(writer http.ResponseWriter, request *http.Req query := fmt.Sprintf("id in (%s)", strings.Join(queryIDs, ", ")) f.logger.Info(fmt.Sprintf("Searching Cluster Service for %q", query)) - listRequest := f.clusterServiceClient.GetConn().ClustersMgmt().V1().Clusters().List().Search(query) - - // XXX This SHOULD avoid dealing with pagination from Cluster Service. - // As far I can tell, uhc-cluster-service does not impose its own - // limit on the page size. Further testing is needed to verify. - listRequest.Size(len(documentMap)) - - listResponse, err := listRequest.SendContext(ctx) - if err != nil { - f.logger.Error(err.Error()) - arm.WriteInternalServerError(writer) - return - } - - for _, csCluster := range listResponse.Items().Slice() { - if doc, ok := documentMap[csCluster.ID()]; ok { - value, err := marshalCSCluster(csCluster, doc, versionedInterface) - if err != nil { - f.logger.Error(err.Error()) - arm.WriteInternalServerError(writer) - return + switch resourceTypeName { + case strings.ToLower(api.ClusterResourceTypeName): + csIterator := f.clusterServiceClient.ListCSClusters(query) + + for csCluster := range csIterator.Items(ctx) { + if doc, ok := documentMap[csCluster.ID()]; ok { + value, err := marshalCSCluster(csCluster, doc, versionedInterface) + if err != nil { + f.logger.Error(err.Error()) + arm.WriteInternalServerError(writer) + return + } + pagedResponse.AddValue(value) } - pagedResponse.AddValue(value) } - } + err = csIterator.GetError() + + case strings.ToLower(api.NodePoolResourceTypeName): + var resourceDoc *database.ResourceDocument - if continuationToken != nil { - err = pagedResponse.SetNextLink(request.Referer(), *continuationToken) + // Fetch the cluster document for the Cluster Service ID. + resourceDoc, err = f.dbClient.GetResourceDoc(ctx, prefix) if err != nil { f.logger.Error(err.Error()) arm.WriteInternalServerError(writer) return } + + csIterator := f.clusterServiceClient.ListCSNodePools(resourceDoc.InternalID, query) + + for csNodePool := range csIterator.Items(ctx) { + if doc, ok := documentMap[csNodePool.ID()]; ok { + value, err := marshalCSNodePool(csNodePool, doc, versionedInterface) + if err != nil { + f.logger.Error(err.Error()) + arm.WriteInternalServerError(writer) + return + } + pagedResponse.AddValue(value) + } + } + err = csIterator.GetError() + + default: + err = fmt.Errorf("unsupported resource type: %s", resourceTypeName) + } + + // Check for iteration error. + if err != nil { + f.logger.Error(err.Error()) + arm.WriteInternalServerError(writer) + return + } + + err = pagedResponse.SetNextLink(request.Referer(), dbIterator.GetContinuationToken()) + if err != nil { + f.logger.Error(err.Error()) + arm.WriteInternalServerError(writer) + return } _, err = arm.WriteJSONResponse(writer, http.StatusOK, pagedResponse) diff --git a/frontend/pkg/frontend/middleware_resourceid_test.go b/frontend/pkg/frontend/middleware_resourceid_test.go index 91aa4d202..f94b629eb 100644 --- a/frontend/pkg/frontend/middleware_resourceid_test.go +++ b/frontend/pkg/frontend/middleware_resourceid_test.go @@ -61,6 +61,17 @@ func TestMiddlewareResourceID(t *testing.T) { "Microsoft.Resources/tenants", }, }, + { + name: "node pool collection", + path: "/SUBSCRIPTIONS/00000000-0000-0000-0000-000000000000/RESOURCEGROUPS/MyResourceGroup/PROVIDERS/MICROSOFT.REDHATOPENSHIFT/HCPOPENSHIFTCLUSTERS/myCluster/NODEPOOLS", + resourceTypes: []string{ + "MICROSOFT.REDHATOPENSHIFT/HCPOPENSHIFTCLUSTERS/NODEPOOLS", + "MICROSOFT.REDHATOPENSHIFT/HCPOPENSHIFTCLUSTERS", + "Microsoft.Resources/resourceGroups", + "Microsoft.Resources/subscriptions", + "Microsoft.Resources/tenants", + }, + }, { name: "preflight deployment", path: "/SUBSCRIPTIONS/00000000-0000-0000-0000-000000000000/RESOURCEGROUPS/MyResourceGroup/PROVIDERS/MICROSOFT.REDHATOPENSHIFT/DEPLOYMENTS/preflight", diff --git a/frontend/pkg/frontend/middleware_validatestatic.go b/frontend/pkg/frontend/middleware_validatestatic.go index 4f172d898..833e0bac3 100644 --- a/frontend/pkg/frontend/middleware_validatestatic.go +++ b/frontend/pkg/frontend/middleware_validatestatic.go @@ -18,8 +18,6 @@ import ( var rxHCPOpenShiftClusterResourceName = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-]{2,53}$`) var rxNodePoolResourceName = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-]{2,14}$`) -var resourceTypeSubscription = "Microsoft.Resources/subscriptions" - func MiddlewareValidateStatic(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { // To conform with "OAPI012: Resource IDs must not be case sensitive" // we need to use the original, non-lowercased resource ID components @@ -40,29 +38,28 @@ func MiddlewareValidateStatic(w http.ResponseWriter, r *http.Request, next http. } } - // Skip static validation for subscription resources - if !strings.EqualFold(resource.ResourceType.String(), resourceTypeSubscription) { - switch strings.ToLower(resource.ResourceType.Type) { - case strings.ToLower(api.ClusterResourceType.Type): - if !rxHCPOpenShiftClusterResourceName.MatchString(resource.Name) { - arm.WriteError(w, http.StatusBadRequest, - arm.CloudErrorCodeInvalidResourceName, - resource.String(), - "The Resource '%s/%s' under resource group '%s' does not conform to the naming restriction.", - resource.ResourceType, resource.Name, - resource.ResourceGroupName) - return - } - case strings.ToLower(api.NodePoolResourceType.Type): - if !rxNodePoolResourceName.MatchString(resource.Name) { - arm.WriteError(w, http.StatusBadRequest, - arm.CloudErrorCodeInvalidResourceName, - resource.String(), - "The Resource '%s/%s' under resource group '%s' does not conform to the naming restriction.", - resource.ResourceType, resource.Name, - resource.ResourceGroupName) - return - } + switch strings.ToLower(resource.ResourceType.Type) { + case strings.ToLower(api.ClusterResourceType.Type): + if !rxHCPOpenShiftClusterResourceName.MatchString(resource.Name) { + arm.WriteError(w, http.StatusBadRequest, + arm.CloudErrorCodeInvalidResourceName, + resource.String(), + "The Resource '%s/%s' under resource group '%s' does not conform to the naming restriction.", + resource.ResourceType, resource.Name, + resource.ResourceGroupName) + return + } + case strings.ToLower(api.NodePoolResourceType.Type): + // The collection GET endpoint for nested resources + // parses into a ResourceID with an empty Name field. + if resource.Name != "" && !rxNodePoolResourceName.MatchString(resource.Name) { + arm.WriteError(w, http.StatusBadRequest, + arm.CloudErrorCodeInvalidResourceName, + resource.String(), + "The Resource '%s/%s' under resource group '%s' does not conform to the naming restriction.", + resource.ResourceType, resource.Name, + resource.ResourceGroupName) + return } } } diff --git a/frontend/pkg/frontend/routes.go b/frontend/pkg/frontend/routes.go index 70bc9e34c..371729015 100644 --- a/frontend/pkg/frontend/routes.go +++ b/frontend/pkg/frontend/routes.go @@ -68,6 +68,9 @@ func (f *Frontend) routes() *MiddlewareMux { mux.Handle( MuxPattern(http.MethodGet, PatternSubscriptions, PatternResourceGroups, PatternProviders, api.ClusterResourceTypeName), postMuxMiddleware.HandlerFunc(f.ArmResourceList)) + mux.Handle( + MuxPattern(http.MethodGet, PatternSubscriptions, PatternResourceGroups, PatternProviders, PatternClusters, api.NodePoolResourceTypeName), + postMuxMiddleware.HandlerFunc(f.ArmResourceList)) // Resource ID endpoints // Request context holds an azcorearm.ResourceID diff --git a/internal/database/cache.go b/internal/database/cache.go index 63b6ca620..a3679e397 100644 --- a/internal/database/cache.go +++ b/internal/database/cache.go @@ -9,8 +9,6 @@ import ( "iter" "strings" - azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" - "github.com/Azure/ARO-HCP/internal/api/arm" ) @@ -24,14 +22,14 @@ type Cache struct { subscription map[string]*SubscriptionDocument } -type operationCacheIterator struct { - operation map[string]*OperationDocument - err error +type cacheIterator struct { + docs []any + err error } -func (iter operationCacheIterator) Items(ctx context.Context) iter.Seq[[]byte] { +func (iter cacheIterator) Items(ctx context.Context) iter.Seq[[]byte] { return func(yield func([]byte) bool) { - for _, doc := range iter.operation { + for _, doc := range iter.docs { // Marshalling the document struct only to immediately unmarshal // it back to a document struct is a little silly but this is to // conform to the DBClientIterator interface. @@ -48,7 +46,11 @@ func (iter operationCacheIterator) Items(ctx context.Context) iter.Seq[[]byte] { } } -func (iter operationCacheIterator) GetError() error { +func (iter cacheIterator) GetContinuationToken() string { + return "" +} + +func (iter cacheIterator) GetError() error { return iter.err } @@ -108,21 +110,19 @@ func (c *Cache) DeleteResourceDoc(ctx context.Context, resourceID *arm.ResourceI return nil } -func (c *Cache) ListResourceDocs(ctx context.Context, prefix *arm.ResourceID, resourceType *azcorearm.ResourceType, pageSizeHint int32, continuationToken *string) ([]*ResourceDocument, *string, error) { - var resourceList []*ResourceDocument +func (c *Cache) ListResourceDocs(ctx context.Context, prefix *arm.ResourceID, maxItems int32, continuationToken *string) DBClientIterator { + var iterator cacheIterator // Make sure key prefix is lowercase. prefixString := strings.ToLower(prefix.String() + "/") for key, doc := range c.resource { if strings.HasPrefix(key, prefixString) { - if resourceType == nil || strings.EqualFold(resourceType.String(), doc.Key.ResourceType.String()) { - resourceList = append(resourceList, doc) - } + iterator.docs = append(iterator.docs, doc) } } - return resourceList, nil, nil + return iterator } func (c *Cache) GetOperationDoc(ctx context.Context, operationID string) (*OperationDocument, error) { @@ -164,7 +164,11 @@ func (c *Cache) DeleteOperationDoc(ctx context.Context, operationID string) erro } func (c *Cache) ListAllOperationDocs(ctx context.Context) DBClientIterator { - return operationCacheIterator{operation: c.operation} + var iterator cacheIterator + for _, doc := range c.operation { + iterator.docs = append(iterator.docs, doc) + } + return iterator } func (c *Cache) GetSubscriptionDoc(ctx context.Context, subscriptionID string) (*SubscriptionDocument, error) { diff --git a/internal/database/database.go b/internal/database/database.go index 85b7c7348..f7575fdd0 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -13,7 +13,6 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" "github.com/Azure/ARO-HCP/internal/api/arm" @@ -51,6 +50,7 @@ func isResponseError(err error, statusCode int) bool { type DBClientIterator interface { Items(ctx context.Context) iter.Seq[[]byte] + GetContinuationToken() string GetError() error } @@ -71,7 +71,7 @@ type DBClient interface { // DeleteResourceDoc deletes a ResourceDocument from the database given the resourceID // of a Microsoft.RedHatOpenShift/HcpOpenShiftClusters resource or NodePools child resource. DeleteResourceDoc(ctx context.Context, resourceID *arm.ResourceID) error - ListResourceDocs(ctx context.Context, prefix *arm.ResourceID, resourceType *azcorearm.ResourceType, pageSizeHint int32, continuationToken *string) ([]*ResourceDocument, *string, error) + ListResourceDocs(ctx context.Context, prefix *arm.ResourceID, maxItems int32, continuationToken *string) DBClientIterator GetOperationDoc(ctx context.Context, operationID string) (*OperationDocument, error) CreateOperationDoc(ctx context.Context, doc *OperationDocument) error @@ -269,13 +269,23 @@ func (d *CosmosDBClient) DeleteResourceDoc(ctx context.Context, resourceID *arm. return nil } -func (d *CosmosDBClient) ListResourceDocs(ctx context.Context, prefix *arm.ResourceID, resourceType *azcorearm.ResourceType, pageSizeHint int32, continuationToken *string) ([]*ResourceDocument, *string, error) { +// ListResourceDocs searches for resource documents that match the given resource ID prefix. +// maxItems can limit the number of items returned at once. A negative value will cause the +// returned iterator to yield all matching items. A positive value will cause the returned +// iterator to include a continuation token if additional items are available. +func (d *CosmosDBClient) ListResourceDocs(ctx context.Context, prefix *arm.ResourceID, maxItems int32, continuationToken *string) DBClientIterator { // Make sure partition key is lowercase. pk := azcosmos.NewPartitionKeyString(strings.ToLower(prefix.SubscriptionID)) + // XXX The Cosmos DB REST API gives special meaning to -1 for "x-ms-max-item-count" + // but it's not clear if it treats all negative values equivalently. The Go SDK + // passes the PageSizeHint value as provided so normalize negative values to -1 + // to be safe. + maxItems = max(maxItems, -1) + query := "SELECT * FROM c WHERE STARTSWITH(c.key, @prefix, true)" opt := azcosmos.QueryOptions{ - PageSizeHint: pageSizeHint, + PageSizeHint: maxItems, ContinuationToken: continuationToken, QueryParameters: []azcosmos.QueryParameter{ { @@ -285,39 +295,13 @@ func (d *CosmosDBClient) ListResourceDocs(ctx context.Context, prefix *arm.Resou }, } - var response azcosmos.QueryItemsResponse - resourceDocs := make([]*ResourceDocument, 0, pageSizeHint) - - // Loop until we fill the pre-allocated resourceDocs slice, - // or until we run out of items from the resources container. - for opt.PageSizeHint > 0 { - var err error - - response, err = d.resources.NewQueryItemsPager(query, pk, &opt).NextPage(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to advance page while querying Resources container for items with a key prefix of '%s': %w", prefix, err) - } - - for _, item := range response.Items { - var doc ResourceDocument - err = json.Unmarshal(item, &doc) - if err != nil { - return nil, nil, fmt.Errorf("failed to unmarshal item while querying Resources container for items with a key prefix of '%s': %w", prefix, err) - } - if resourceType == nil || strings.EqualFold(resourceType.String(), doc.Key.ResourceType.String()) { - resourceDocs = append(resourceDocs, &doc) - } - } - - if response.ContinuationToken == nil { - break - } + pager := d.resources.NewQueryItemsPager(query, pk, &opt) - opt.PageSizeHint = int32(cap(resourceDocs) - len(resourceDocs)) - opt.ContinuationToken = response.ContinuationToken + if maxItems > 0 { + return NewQueryItemsSinglePageIterator(pager) + } else { + return NewQueryItemsIterator(pager) } - - return resourceDocs, response.ContinuationToken, nil } // GetOperationDoc retrieves the asynchronous operation document for the given diff --git a/internal/database/util.go b/internal/database/util.go index 2ccbec4e4..df8abf006 100644 --- a/internal/database/util.go +++ b/internal/database/util.go @@ -12,8 +12,10 @@ import ( ) type QueryItemsIterator struct { - pager *runtime.Pager[azcosmos.QueryItemsResponse] - err error + pager *runtime.Pager[azcosmos.QueryItemsResponse] + singlePage bool + continuationToken string + err error } // NewQueryItemsIterator is a failable push iterator for a paged query response. @@ -21,6 +23,13 @@ func NewQueryItemsIterator(pager *runtime.Pager[azcosmos.QueryItemsResponse]) Qu return QueryItemsIterator{pager: pager} } +// NewQueryItemsSinglePageIterator is a failable push iterator for a paged +// query response that stops at the end of the first page and includes a +// continuation token if additional items are available. +func NewQueryItemsSinglePageIterator(pager *runtime.Pager[azcosmos.QueryItemsResponse]) QueryItemsIterator { + return QueryItemsIterator{pager: pager, singlePage: true} +} + // Items returns a push iterator that can be used directly in for/range loops. // If an error occurs during paging, iteration stops and the error is recorded. func (iter QueryItemsIterator) Items(ctx context.Context) iter.Seq[[]byte] { @@ -31,15 +40,28 @@ func (iter QueryItemsIterator) Items(ctx context.Context) iter.Seq[[]byte] { iter.err = err return } + if iter.singlePage && response.ContinuationToken != nil { + iter.continuationToken = *response.ContinuationToken + } for _, item := range response.Items { if !yield(item) { return } } + if iter.singlePage { + return + } } } } +// GetContinuationToken returns a continuation token that can be used to obtain +// the next page of results. This is only set when the iterator was created with +// NewQueryItemsSinglePageIterator and additional items are available. +func (iter QueryItemsIterator) GetContinuationToken() string { + return iter.continuationToken +} + // GetError returns any error that occurred during iteration. Call this after the // for/range loop that calls Items() to check if iteration completed successfully. func (iter QueryItemsIterator) GetError() error { diff --git a/internal/ocm/iterators.go b/internal/ocm/iterators.go new file mode 100644 index 000000000..b867c0da3 --- /dev/null +++ b/internal/ocm/iterators.go @@ -0,0 +1,120 @@ +package ocm + +// Copyright (c) Microsoft Corporation. +// Licensed under the Apache License 2.0. + +import ( + "context" + "iter" + "math" + + cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" +) + +type ClusterListIterator struct { + request *cmv1.ClustersListRequest + err error +} + +// Items returns a push iterator that can be used directly in for/range loops. +// If an error occurs during paging, iteration stops and the error is recorded. +func (iter ClusterListIterator) Items(ctx context.Context) iter.Seq[*cmv1.Cluster] { + return func(yield func(*cmv1.Cluster) bool) { + // Request can be nil to allow for mocking. + if iter.request != nil { + var page int = 0 + var count int = 0 + var total int = math.MaxInt + + for count < total { + page++ + result, err := iter.request.Page(page).SendContext(ctx) + if err != nil { + iter.err = err + return + } + + total = result.Total() + items := result.Items() + + // Safety check to prevent an infinite loop in case + // the result is somehow empty before count = total. + if items == nil || items.Empty() { + return + } + + count += items.Len() + + // XXX ClusterList.Each() lacks a boolean return to + // indicate whether iteration fully completed. + // ClusterList.Slice() may be less efficient but + // is easier to work with. + for _, item := range items.Slice() { + if !yield(item) { + return + } + } + } + } + } +} + +// GetError returns any error that occurred during iteration. Call this after the +// for/range loop that calls Items() to check if iteration completed successfully. +func (iter ClusterListIterator) GetError() error { + return iter.err +} + +type NodePoolListIterator struct { + request *cmv1.NodePoolsListRequest + err error +} + +// Items returns a push iterator that can be used directly in for/range loops. +// If an error occurs during paging, iteration stops and the error is recorded. +func (iter NodePoolListIterator) Items(ctx context.Context) iter.Seq[*cmv1.NodePool] { + return func(yield func(*cmv1.NodePool) bool) { + // Request can be nil to allow for mocking. + if iter.request != nil { + var page int = 0 + var count int = 0 + var total int = math.MaxInt + + for count < total { + page++ + result, err := iter.request.Page(page).SendContext(ctx) + if err != nil { + iter.err = err + return + } + + total = result.Total() + items := result.Items() + + // Safety check to prevent an infinite loop in case + // the result is somehow empty before count = total. + if items == nil || items.Empty() { + return + } + + count += items.Len() + + // XXX NodePoolList.Each() lacks a boolean return to + // indicate whether iteration fully completed. + // NodePoolList.Slice() may be less efficient but + // is easier to work with. + for _, item := range items.Slice() { + if !yield(item) { + return + } + } + } + } + } +} + +// GetError returns any error that occurred during iteration. Call this after the +// for/range loop that calls Items() to check if iteration completed successfully. +func (iter NodePoolListIterator) GetError() error { + return iter.err +} diff --git a/internal/ocm/mock.go b/internal/ocm/mock.go index 2081d2517..fdcde0828 100644 --- a/internal/ocm/mock.go +++ b/internal/ocm/mock.go @@ -91,6 +91,10 @@ func (mcsc *MockClusterServiceClient) DeleteCSCluster(ctx context.Context, inter return nil } +func (mcsc *MockClusterServiceClient) ListCSClusters(searchExpression string) ClusterListIterator { + return ClusterListIterator{err: fmt.Errorf("ListCSClusters not implemented")} +} + func (mcsc *MockClusterServiceClient) GetCSNodePool(ctx context.Context, internalID InternalID) (*cmv1.NodePool, error) { nodePool, ok := mcsc.nodePools[internalID] if !ok { @@ -133,3 +137,7 @@ func (mcsc *MockClusterServiceClient) DeleteCSNodePool(ctx context.Context, inte delete(mcsc.nodePools, internalID) return nil } + +func (mcsc *MockClusterServiceClient) ListCSNodePools(clusterInternalID InternalID, searchExpression string) NodePoolListIterator { + return NodePoolListIterator{err: fmt.Errorf("ListCSClusters not implemented")} +} diff --git a/internal/ocm/ocm.go b/internal/ocm/ocm.go index 6c5dc2fea..3f7e121ac 100644 --- a/internal/ocm/ocm.go +++ b/internal/ocm/ocm.go @@ -18,10 +18,12 @@ type ClusterServiceClientSpec interface { PostCSCluster(ctx context.Context, cluster *cmv1.Cluster) (*cmv1.Cluster, error) UpdateCSCluster(ctx context.Context, internalID InternalID, cluster *cmv1.Cluster) (*cmv1.Cluster, error) DeleteCSCluster(ctx context.Context, internalID InternalID) error + ListCSClusters(searchExpression string) ClusterListIterator GetCSNodePool(ctx context.Context, internalID InternalID) (*cmv1.NodePool, error) PostCSNodePool(ctx context.Context, clusterInternalID InternalID, nodePool *cmv1.NodePool) (*cmv1.NodePool, error) UpdateCSNodePool(ctx context.Context, internalID InternalID, nodePool *cmv1.NodePool) (*cmv1.NodePool, error) DeleteCSNodePool(ctx context.Context, internalID InternalID) error + ListCSNodePools(clusterInternalID InternalID, searchExpression string) NodePoolListIterator } // Get the default set of properties for the Cluster Service @@ -153,6 +155,17 @@ func (csc *ClusterServiceClient) DeleteCSCluster(ctx context.Context, internalID return err } +// ListCSClusters prepares a GET request with the given search expression. Call Items() on +// the returned iterator in a for/range loop to execute the request and paginate over results, +// then call GetError() to check for an iteration error. +func (csc *ClusterServiceClient) ListCSClusters(searchExpression string) ClusterListIterator { + clustersListRequest := csc.Conn.ClustersMgmt().V1().Clusters().List() + if searchExpression != "" { + clustersListRequest.Search(searchExpression) + } + return ClusterListIterator{request: clustersListRequest} +} + // GetCSNodePool creates and sends a GET request to fetch a node pool from Clusters Service func (csc *ClusterServiceClient) GetCSNodePool(ctx context.Context, internalID InternalID) (*cmv1.NodePool, error) { client, ok := internalID.GetNodePoolClient(csc.Conn) @@ -213,3 +226,18 @@ func (csc *ClusterServiceClient) DeleteCSNodePool(ctx context.Context, internalI _, err := client.Delete().SendContext(ctx) return err } + +// ListCSNodePools prepares a GET request with the given search expression. Call Items() on +// the returned iterator in a for/range loop to execute the request and paginate over results, +// then call GetError() to check for an iteration error. +func (csc *ClusterServiceClient) ListCSNodePools(clusterInternalID InternalID, searchExpression string) NodePoolListIterator { + client, ok := clusterInternalID.GetClusterClient(csc.Conn) + if !ok { + return NodePoolListIterator{err: fmt.Errorf("OCM path is not a cluster: %s", clusterInternalID)} + } + nodePoolsListRequest := client.NodePools().List() + if searchExpression != "" { + nodePoolsListRequest.Search(searchExpression) + } + return NodePoolListIterator{request: nodePoolsListRequest} +}