Skip to content

Commit

Permalink
[PLAT-15809] Pagination of Tasks List API
Browse files Browse the repository at this point in the history
Summary:
Add an API to support pagination of Tasks list API

POST API: `http://localhost:9000/api/v1/customers/f33e3c9b-75ab-4c30-80ad-cba85646ea39/tasks_list/page`
Request body: ` {"sortBy":"createTime","direction":"DESC","filter":{},"limit":10,"offset":0,"needTotalCount":true}`

Supported filter params:
  #   dateRangeStart;
  #   dateRangeEnd;
  #   targetList;
  #   targetUUIDList;
  #   typeList;
  #   typeNameList;
  #   status;

Test Plan:
Response body for `{"sortBy":"createTime","direction":"DESC","filter":{"typeList": ["Create"]},"limit":10,"offset":0,"needTotalCount":true}`
```
{
    "entities": [
        {
            "id": "d1ffc733-17a5-4415-ae17-d1c6905f6c6e",
            "title": "Created Provider : dkumar-cli",
            "percentComplete": 100,
            "createTime": "2024-11-15T19:18:39Z",
            "completionTime": "2024-11-15T19:18:56Z",
            "target": "Provider",
            "targetUUID": "55fd63e4-7b66-401f-86fa-074da43a6a02",
            "type": "Create",
            "typeName": "Create",
            "status": "Success",
            "details": {
                "taskDetails": [
                    {
                        "title": "Bootstrapping Cloud",
                        "description": "Set up AccessKey, Region, and Provider for a given cloud Provider.",
                        "state": "Success",
                        "extraDetails": []
                    },
                    {
                        "title": "Bootstrapping Region",
                        "description": "Set up AccessKey, Region, and Provider for a given cloud Provider.",
                        "state": "Success",
                        "extraDetails": []
                    },
                    {
                        "title": "Creating AccessKey",
                        "description": "Set up AccessKey in the given Provider Vault",
                        "state": "Success",
                        "extraDetails": []
                    },
                    {
                        "title": "Configuring",
                        "description": "Applying the configuration.",
                        "state": "Success",
                        "extraDetails": []
                    },
                    {
                        "title": "Initializing Cloud Metadata",
                        "description": "Initialize Instance Pricing and Zone Metadata from Cloud Provider",
                        "state": "Success",
                        "extraDetails": []
                    }
                ]
            },
            "abortable": false,
            "retryable": false,
            "correlationId": "d2fc9ab6-cb21-4073-89b3-f1f82e5ef58d",
            "userEmail": "admin"
        },
        {
            "id": "904abdd1-db51-4aea-a150-736e665851b6",
            "title": "Created Provider : dkumar",
            "percentComplete": 100,
            "createTime": "2024-10-17T11:59:40Z",
            "completionTime": "2024-10-17T11:59:40Z",
            "target": "Provider",
            "targetUUID": "d68fef15-df24-4ca0-992a-90d51c5ffb11",
            "type": "Create",
            "typeName": "Create",
            "status": "Success",
            "details": {
                "taskDetails": []
            },
            "abortable": false,
            "retryable": false,
            "correlationId": "31cef417-3a16-4291-bfba-862fac579e5f",
            "userEmail": "admin"
        },
        {
            "id": "5ef91eff-2161-4978-82d8-6b84bf7c9bdb",
            "title": "Created Provider : dkumar-onprem",
            "percentComplete": 100,
            "createTime": "2024-10-17T11:36:05Z",
            "completionTime": "2024-10-17T11:36:21Z",
            "target": "Provider",
            "targetUUID": "0f032006-e898-4a66-ac59-b4f8b17f1187",
            "type": "Create",
            "typeName": "Create",
            "status": "Success",
            "details": {
                "taskDetails": [
                    {
                        "title": "Bootstrapping Region",
                        "description": "Set up AccessKey, Region, and Provider for a given cloud Provider.",
                        "state": "Success",
                        "extraDetails": []
                    },
                    {
                        "title": "Creating AccessKey",
                        "description": "Set up AccessKey in the given Provider Vault",
                        "state": "Success",
                        "extraDetails": []
                    }
                ]
            },
            "abortable": false,
            "retryable": false,
            "correlationId": "2766fa96-a9f0-47f0-871a-b36fde2e4dc0",
            "userEmail": "admin"
        },
        {
            "id": "b8b6e786-9782-4a66-a60e-afd24fe9ee9e",
            "title": "Created Provider : dkumar",
            "percentComplete": 100,
            "createTime": "2024-09-16T06:08:23Z",
            "completionTime": "2024-09-16T06:08:38Z",
            "target": "Provider",
            "targetUUID": "d93ea39b-f660-4d8d-b103-4687272e8e75",
            "type": "Create",
            "typeName": "Create",
            "status": "Success",
            "details": {
                "taskDetails": [
                    {
                        "title": "Bootstrapping Cloud",
                        "description": "Set up AccessKey, Region, and Provider for a given cloud Provider.",
                        "state": "Success",
                        "extraDetails": []
                    },
                    {
                        "title": "Bootstrapping Region",
                        "description": "Set up AccessKey, Region, and Provider for a given cloud Provider.",
                        "state": "Success",
                        "extraDetails": []
                    },
                    {
                        "title": "Creating AccessKey",
                        "description": "Set up AccessKey in the given Provider Vault",
                        "state": "Success",
                        "extraDetails": []
                    },
                    {
                        "title": "Configuring",
                        "description": "Applying the configuration.",
                        "state": "Success",
                        "extraDetails": []
                    },
                    {
                        "title": "Initializing Cloud Metadata",
                        "description": "Initialize Instance Pricing and Zone Metadata from Cloud Provider",
                        "state": "Success",
                        "extraDetails": []
                    }
                ]
            },
            "abortable": false,
            "retryable": false,
            "correlationId": "6b541ae0-fa87-4da0-8da4-72a74e0116d6",
            "userEmail": "admin"
        }
    ],
    "hasNext": false,
    "hasPrev": false,
    "totalCount": 4
}
```

Reviewers: #yba-api-review, nsingh, sneelakantan

Reviewed By: #yba-api-review, nsingh, sneelakantan

Subscribers: yugaware

Differential Revision: https://phorge.dev.yugabyte.com/D41284
  • Loading branch information
Deepti-yb committed Jan 22, 2025
1 parent 13fb294 commit c2872c4
Show file tree
Hide file tree
Showing 11 changed files with 682 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

package com.yugabyte.yw.controllers;

import static com.yugabyte.yw.models.helpers.CommonUtils.appendInClause;
import static com.yugabyte.yw.models.helpers.CommonUtils.performPagedQuery;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
Expand All @@ -17,22 +20,34 @@
import com.yugabyte.yw.forms.PlatformResults;
import com.yugabyte.yw.forms.PlatformResults.YBPSuccess;
import com.yugabyte.yw.forms.SubTaskFormData;
import com.yugabyte.yw.forms.filters.TaskApiFilter;
import com.yugabyte.yw.forms.paging.TaskPagedApiQuery;
import com.yugabyte.yw.models.Audit;
import com.yugabyte.yw.models.Customer;
import com.yugabyte.yw.models.CustomerTask;
import com.yugabyte.yw.models.TaskInfo;
import com.yugabyte.yw.models.Universe;
import com.yugabyte.yw.models.common.YbaApi;
import com.yugabyte.yw.models.common.YbaApi.YbaApiVisibility;
import com.yugabyte.yw.models.filters.TaskFilter;
import com.yugabyte.yw.models.helpers.FailedSubtasks;
import com.yugabyte.yw.models.helpers.YBAError;
import com.yugabyte.yw.models.paging.PagedQuery;
import com.yugabyte.yw.models.paging.PagedQuery.SortByIF;
import com.yugabyte.yw.models.paging.PagedQuery.SortDirection;
import com.yugabyte.yw.models.paging.TaskPagedApiResponse;
import com.yugabyte.yw.models.paging.TaskPagedQuery;
import com.yugabyte.yw.models.paging.TaskPagedResponse;
import com.yugabyte.yw.rbac.annotations.AuthzPath;
import com.yugabyte.yw.rbac.annotations.PermissionAttribute;
import com.yugabyte.yw.rbac.annotations.RequiredPermissionOnResource;
import com.yugabyte.yw.rbac.annotations.Resource;
import com.yugabyte.yw.rbac.enums.SourceType;
import io.ebean.ExpressionList;
import io.ebean.Query;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.Authorization;
import java.util.ArrayList;
Expand All @@ -42,11 +57,13 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.libs.Json;
Expand Down Expand Up @@ -176,23 +193,23 @@ private Map<UUID, List<CustomerTaskFormData>> fetchTasks(Customer customer, UUID
return buildTaskListMap(customer, customerTaskList);
}

private Map<UUID, List<CustomerTaskFormData>> buildTaskListMap(
Customer customer, List<CustomerTask> customerTaskList) {
private Map<UUID, CustomerTask> buildLastTaskByTargetMap(List<CustomerTask> customerTaskList) {
return customerTaskList.stream()
.filter(c -> c.getCompletionTime() != null)
.collect(
Collectors.toMap(
CustomerTask::getTargetUUID,
Function.identity(),
(c1, c2) -> c1.getCompletionTime().after(c2.getCompletionTime()) ? c1 : c2));
}

Map<UUID, List<CustomerTaskFormData>> taskListMap = new HashMap<>();
Map<UUID, CustomerTask> lastTaskByTargetMap =
customerTaskList.stream()
.filter(c -> c.getCompletionTime() != null)
.collect(
Collectors.toMap(
CustomerTask::getTargetUUID,
Function.identity(),
(c1, c2) -> c1.getCompletionTime().after(c2.getCompletionTime()) ? c1 : c2));
private Map<UUID, Set<String>> buildAllowRetryTasksByTargetMap(Customer customer) {
Map<UUID, Set<String>> allowRetryTasksByTargetMap = new HashMap<>();
Map<UUID, String> updatingTaskByTargetMap =
commissioner.getUpdatingTaskUUIDsForTargets(customer.getId());
Map<UUID, String> placementModificationTaskByTargetMap =
commissioner.getPlacementModificationTaskUUIDsForTargets(customer.getId());
Map<UUID, Set<String>> allowRetryTasksByTargetMap = new HashMap<>();

updatingTaskByTargetMap.forEach(
(universeUUID, taskUUID) ->
allowRetryTasksByTargetMap
Expand All @@ -203,6 +220,16 @@ private Map<UUID, List<CustomerTaskFormData>> buildTaskListMap(
allowRetryTasksByTargetMap
.computeIfAbsent(universeUUID, k -> new HashSet<>())
.add(taskUUID));

return allowRetryTasksByTargetMap;
}

private Map<UUID, List<CustomerTaskFormData>> buildTaskListMap(
Customer customer, List<CustomerTask> customerTaskList) {

Map<UUID, List<CustomerTaskFormData>> taskListMap = new HashMap<>();
Map<UUID, CustomerTask> lastTaskByTargetMap = buildLastTaskByTargetMap(customerTaskList);
Map<UUID, Set<String>> allowRetryTasksByTargetMap = buildAllowRetryTasksByTargetMap(customer);
List<List<CustomerTask>> batches =
Lists.partition(
customerTaskList,
Expand Down Expand Up @@ -232,6 +259,85 @@ private Map<UUID, List<CustomerTaskFormData>> buildTaskListMap(
return taskListMap;
}

public enum SortBy implements PagedQuery.SortByIF {
createTime("createTime");

private final String sortField;

SortBy(String sortField) {
this.sortField = sortField;
}

public String getSortField() {
return sortField;
}

@Override
public SortByIF getOrderField() {
return SortBy.createTime;
}
}

public TaskPagedApiResponse pagedList(TaskPagedQuery pagedQuery, Customer customer) {
if (pagedQuery.getSortBy() == null) {
pagedQuery.setSortBy(SortBy.createTime);
pagedQuery.setDirection(SortDirection.DESC);
}
Query<CustomerTask> query = createQueryByFilter(pagedQuery.getFilter()).query();
TaskPagedResponse response = performPagedQuery(query, pagedQuery, TaskPagedResponse.class);
return createResponse(response, customer);
}

public ExpressionList<CustomerTask> createQueryByFilter(TaskFilter filter) {

ExpressionList<CustomerTask> query = CustomerTask.find.query().where();

query.eq("customer_uuid", filter.getCustomerUUID());
if (!CollectionUtils.isEmpty(filter.getTargetList())) {
appendInClause(query, "target", filter.getTargetList());
}
if (!CollectionUtils.isEmpty(filter.getTargetUUIDList())) {
appendInClause(query, "target_uuid", filter.getTargetUUIDList());
}
if (!CollectionUtils.isEmpty(filter.getTypeList())) {
appendInClause(query, "type", filter.getTypeList());
}
if (!CollectionUtils.isEmpty(filter.getTypeNameList())) {
appendInClause(query, "type_name", filter.getTypeNameList());
}
if (filter.getDateRangeStart() != null && filter.getDateRangeEnd() != null) {
query.between("create_time", filter.getDateRangeStart(), filter.getDateRangeEnd());
}
if (!CollectionUtils.isEmpty(filter.getStatus())) {
appendInClause(query, "status", filter.getStatus());
}
return query;
}

public TaskPagedApiResponse createResponse(TaskPagedResponse response, Customer customer) {
List<CustomerTask> tasks = response.getEntities();
Map<UUID, List<TaskInfo>> subTaskInfos =
TaskInfo.getSubTasks(
tasks.stream().map(CustomerTask::getTaskUUID).collect(Collectors.toSet()));
Map<UUID, CustomerTask> lastTaskByTargetMap = buildLastTaskByTargetMap(tasks);
Map<UUID, Set<String>> allowRetryTasksByTargetMap = buildAllowRetryTasksByTargetMap(customer);
List<CustomerTaskFormData> taskList =
tasks.parallelStream()
.map(
r ->
commissioner
.buildTaskStatus(
r,
subTaskInfos.getOrDefault(r.getTaskUUID(), Collections.emptyList()),
allowRetryTasksByTargetMap,
lastTaskByTargetMap)
.map(taskProgress -> buildCustomerTaskFromData(r, taskProgress))
.orElse(null))
.filter(Objects::nonNull)
.collect(Collectors.toList());
return response.setData(taskList, new TaskPagedApiResponse());
}

@ApiOperation(value = "UI_ONLY", hidden = true)
@AuthzPath({
@RequiredPermissionOnResource(
Expand Down Expand Up @@ -265,6 +371,37 @@ public Result tasksList(UUID customerUUID, UUID universeUUID) {
return PlatformResults.withData(flattenList);
}

@ApiOperation(
notes = "WARNING: This is a preview API that could change.",
value = "List Tasks (paginated)",
response = TaskPagedApiResponse.class,
nickname = "listTasksV2")
@ApiImplicitParams(
@ApiImplicitParam(
name = "PageTasksRequest",
paramType = "body",
dataType = "com.yugabyte.yw.forms.paging.TaskPagedApiQuery",
required = true))
@YbaApi(visibility = YbaApi.YbaApiVisibility.PREVIEW, sinceYBAVersion = "2.25.1.0")
@AuthzPath({
@RequiredPermissionOnResource(
requiredPermission =
@PermissionAttribute(resourceType = ResourceType.OTHER, action = Action.READ),
resourceLocation = @Resource(path = Util.CUSTOMERS, sourceType = SourceType.ENDPOINT))
})
public Result pageTaskList(UUID customerUUID, Http.Request request) {
Customer customer = Customer.getOrBadRequest(customerUUID);

TaskPagedApiQuery apiQuery = parseJsonAndValidate(request, TaskPagedApiQuery.class);
TaskApiFilter apiFilter = apiQuery.getFilter();
TaskFilter filter = apiFilter.toFilter().toBuilder().customerUUID(customerUUID).build();
TaskPagedQuery query = apiQuery.copyWithFilter(filter, TaskPagedQuery.class);

TaskPagedApiResponse tasks = pagedList(query, customer);

return PlatformResults.withData(tasks);
}

@ApiOperation(value = "UI_ONLY", hidden = true)
@AuthzPath({
@RequiredPermissionOnResource(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) YugaByte, Inc.

package com.yugabyte.yw.forms.filters;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.yugabyte.yw.models.CustomerTask;
import com.yugabyte.yw.models.filters.TaskFilter;
import io.swagger.annotations.ApiModelProperty;
import java.util.Date;
import java.util.Set;
import java.util.UUID;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;

@Data
@NoArgsConstructor
public class TaskApiFilter {

@ApiModelProperty(
value = "The start date to filter paged query.",
example = "2022-12-12T13:07:18Z")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
private Date dateRangeStart;

@ApiModelProperty(value = "The end date to filter paged query.", example = "2022-12-12T13:07:18Z")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
private Date dateRangeEnd;

private Set<CustomerTask.TargetType> targetList;
private Set<UUID> targetUUIDList;
private Set<CustomerTask.TaskType> typeList;
private Set<String> typeNameList;
private Set<String> status;

public TaskFilter toFilter() {
TaskFilter.TaskFilterBuilder builder = TaskFilter.builder();
if (!CollectionUtils.isEmpty(targetList)) {
builder.targetList(targetList);
}
if (!CollectionUtils.isEmpty(targetUUIDList)) {
builder.targetUUIDList(targetUUIDList);
}
if (!CollectionUtils.isEmpty(typeList)) {
builder.typeList(typeList);
}
if (!CollectionUtils.isEmpty(typeNameList)) {
builder.typeNameList(typeNameList);
}
if (!CollectionUtils.isEmpty(status)) {
builder.status(status);
}
if (dateRangeEnd != null) {
builder.dateRangeEnd(dateRangeEnd);
}
if (dateRangeStart != null) {
builder.dateRangeStart(dateRangeStart);
}
return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) YugaByte, Inc.

package com.yugabyte.yw.forms.paging;

import com.yugabyte.yw.controllers.CustomerTaskController;
import com.yugabyte.yw.forms.filters.TaskApiFilter;
import com.yugabyte.yw.models.paging.PagedQuery;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = true)
public class TaskPagedApiQuery extends PagedQuery<TaskApiFilter, CustomerTaskController.SortBy> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) YugaByte, Inc
package com.yugabyte.yw.models.filters;

import com.yugabyte.yw.models.CustomerTask;
import java.util.Date;
import java.util.Set;
import java.util.UUID;
import lombok.Builder;
import lombok.Value;

@Value
@Builder(toBuilder = true)
public class TaskFilter {
Date dateRangeStart;
Date dateRangeEnd;
Set<CustomerTask.TargetType> targetList;
Set<UUID> targetUUIDList;
Set<CustomerTask.TaskType> typeList;
Set<String> typeNameList;
Set<String> status;
UUID customerUUID;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) YugaByte, Inc

package com.yugabyte.yw.models.paging;

import com.yugabyte.yw.forms.CustomerTaskFormData;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = true)
public class TaskPagedApiResponse extends PagedResponse<CustomerTaskFormData> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) YugaByte, Inc

package com.yugabyte.yw.models.paging;

import com.yugabyte.yw.controllers.CustomerTaskController;
import com.yugabyte.yw.models.filters.TaskFilter;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = true)
public class TaskPagedQuery extends PagedQuery<TaskFilter, CustomerTaskController.SortBy> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) YugaByte, Inc.

package com.yugabyte.yw.models.paging;

import com.yugabyte.yw.models.CustomerTask;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = true)
public class TaskPagedResponse extends PagedResponse<CustomerTask> {}
Loading

0 comments on commit c2872c4

Please sign in to comment.