diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java index c7b28c2c02..2abab0717c 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java @@ -139,6 +139,21 @@ public interface TargetManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) long countByRsqlAndCompatible(@NotEmpty String rsqlParam, @NotNull Long dsTypeId); + /** + * Count all targets with failed actions for specific Rollout + * and that are compatible with the passed {@link DistributionSetType} + * and created after given timestamp + * + * @param rolloutId + * rolloutId of the rollout to be retried. + * @param dsTypeId + * ID of the {@link DistributionSetType} the targets need to be + * compatible with + * @return the found number of{@link Target}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + long countByFailedInRollout(@NotEmpty String rolloutId, @NotNull Long dsTypeId); + /** * Count {@link TargetFilterQuery}s for given target filter query. * @@ -278,6 +293,23 @@ Slice findByTargetFilterQueryAndNotInRolloutGroupsAndCompatible(@NotNull @NotEmpty Collection groups, @NotNull String rsqlParam, @NotNull DistributionSetType distributionSetType); + /** + * Finds all targets with failed actions for specific Rollout + * and that are not assigned to one of the retried {@link RolloutGroup}s and are + * compatible with the passed {@link DistributionSetType}. + * + * @param pageRequest + * the pageRequest to enhance the query for paging and sorting + * @param groups + * the list of {@link RolloutGroup}s + * @param rolloutId + * rolloutId of the rollout to be retried. + * @return a page of the found {@link Target}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + Slice findByFailedRolloutAndNotInRolloutGroups(@NotNull Pageable pageRequest, + @NotEmpty Collection groups, @NotNull String rolloutId); + /** * Counts all targets for all the given parameter {@link TargetFilterQuery} * and that are not assigned to one of the {@link RolloutGroup}s and are @@ -296,6 +328,20 @@ Slice findByTargetFilterQueryAndNotInRolloutGroupsAndCompatible(@NotNull long countByRsqlAndNotInRolloutGroupsAndCompatible(@NotEmpty Collection groups, @NotNull String rsqlParam, @NotNull DistributionSetType distributionSetType); + /** + * Counts all targets with failed actions for specific Rollout + * and that are not assigned to one of the {@link RolloutGroup}s and are + * compatible with the passed {@link DistributionSetType}. + * + * @param groups + * the list of {@link RolloutGroup}s + * @param rolloutId + * rolloutId of the rollout to be retried. + * @return count of the found {@link Target}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + long countByFailedRolloutAndNotInRolloutGroups(@NotEmpty Collection groups, @NotNull String rolloutId); + /** * Finds all targets of the provided {@link RolloutGroup} that have no * Action for the RolloutGroup. diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutHelper.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutHelper.java index fa6409bed0..9680949f61 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutHelper.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutHelper.java @@ -229,6 +229,9 @@ public static String getGroupTargetFilter(final String baseFilter, final Rollout if (StringUtils.isEmpty(group.getTargetFilterQuery())) { return baseFilter; } + if (isRolloutRetried(baseFilter)) { + return baseFilter; + } return concatAndTargetFilters(baseFilter, group.getTargetFilterQuery()); } @@ -253,4 +256,12 @@ public static void checkIfRolloutCanStarted(final Rollout rollout, final Rollout + rollout.getStatus().name().toLowerCase()); } } + + public static boolean isRolloutRetried(final String targetFilter) { + return targetFilter.contains("failedrollout"); + } + + public static String getIdFromRetriedTargetFilter(final String targetFilter) { + return targetFilter.substring("failedrollout==".length()); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java index e23cf7c69b..f235b729a2 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java @@ -525,10 +525,18 @@ private RolloutGroup fillRolloutGroupWithTargets(final JpaRollout rollout, final final List readyGroups = RolloutHelper.getGroupsByStatusIncludingGroup(rollout.getRolloutGroups(), RolloutGroupStatus.READY, group); - final long targetsInGroupFilter = DeploymentHelper.runInNewTransaction(txManager, + long targetsInGroupFilter; + if (!RolloutHelper.isRolloutRetried(rollout.getTargetFilterQuery())) { + targetsInGroupFilter = DeploymentHelper.runInNewTransaction(txManager, "countAllTargetsByTargetFilterQueryAndNotInRolloutGroups", count -> targetManagement.countByRsqlAndNotInRolloutGroupsAndCompatible(readyGroups, groupTargetFilter, - rollout.getDistributionSet().getType())); + rollout.getDistributionSet().getType())); + } else { + targetsInGroupFilter = DeploymentHelper.runInNewTransaction(txManager, + "countByFailedRolloutAndNotInRolloutGroupsAndCompatible", + count -> targetManagement.countByFailedRolloutAndNotInRolloutGroups(readyGroups, + RolloutHelper.getIdFromRetriedTargetFilter(rollout.getTargetFilterQuery()))); + } final long expectedInGroup = Math .round((double) (group.getTargetPercentage() / 100) * (double) targetsInGroupFilter); final long currentlyInGroup = DeploymentHelper.runInNewTransaction(txManager, @@ -572,8 +580,14 @@ private Long assignTargetsToGroupInNewTransaction(final JpaRollout rollout, fina final PageRequest pageRequest = PageRequest.of(0, Math.toIntExact(limit)); final List readyGroups = RolloutHelper.getGroupsByStatusIncludingGroup(rollout.getRolloutGroups(), RolloutGroupStatus.READY, group); - final Slice targets = targetManagement.findByTargetFilterQueryAndNotInRolloutGroupsAndCompatible( + Slice targets; + if (!RolloutHelper.isRolloutRetried(rollout.getTargetFilterQuery())) { + targets = targetManagement.findByTargetFilterQueryAndNotInRolloutGroupsAndCompatible( pageRequest, readyGroups, targetFilter, rollout.getDistributionSet().getType()); + } else { + targets = targetManagement.findByFailedRolloutAndNotInRolloutGroups( + pageRequest, readyGroups, RolloutHelper.getIdFromRetriedTargetFilter(rollout.getTargetFilterQuery())); + } createAssignmentOfTargetsToGroup(targets, group); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java index 0dee3ed462..5b3a0531c0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java @@ -196,10 +196,20 @@ public Rollout create(final RolloutCreate rollout, final List groups, final String baseFilter, final long totalTargets, final Long dsTypeId) { final List groupTargetCounts = new ArrayList<>(groups.size()); - final Map targetFilterCounts = groups.stream() + Map targetFilterCounts; + if (!RolloutHelper.isRolloutRetried(baseFilter)) { + targetFilterCounts = groups.stream() + .map(group -> RolloutHelper.getGroupTargetFilter(baseFilter, group)).distinct() + .collect(Collectors.toMap(Function.identity(), + groupTargetFilter -> targetManagement.countByRsqlAndCompatible(groupTargetFilter, dsTypeId))); + } else { + targetFilterCounts = groups.stream() .map(group -> RolloutHelper.getGroupTargetFilter(baseFilter, group)).distinct() .collect(Collectors.toMap(Function.identity(), - groupTargetFilter -> targetManagement.countByRsqlAndCompatible(groupTargetFilter, dsTypeId))); + groupTargetFilter -> targetManagement.countByFailedInRollout( + RolloutHelper.getIdFromRetriedTargetFilter(baseFilter), dsTypeId))); + } long unusedTargetsCount = 0; @@ -675,8 +694,11 @@ private long countOverlappingTargetsWithPreviousGroups(final String baseFilter, private long calculateRemainingTargets(final List groups, final String targetFilter, final Long createdAt, final Long dsTypeId) { - final String baseFilter = RolloutHelper.getTargetFilterQuery(targetFilter, createdAt); - final long totalTargets = targetManagement.countByRsqlAndCompatible(baseFilter, dsTypeId); + + final TargetCount targets = calculateTargets(targetFilter, createdAt, dsTypeId); + long totalTargets = targets.total(); + final String baseFilter = targets.filter(); + if (totalTargets == 0) { throw new ConstraintDeclarationException("Rollout target filter does not match any targets"); } @@ -691,9 +713,9 @@ private long calculateRemainingTargets(final List groups, final St public ListenableFuture validateTargetsInGroups(final List groups, final String targetFilter, final Long createdAt, final Long dsTypeId) { - final String baseFilter = RolloutHelper.getTargetFilterQuery(targetFilter, createdAt); - - final long totalTargets = targetManagement.countByRsqlAndCompatible(baseFilter, dsTypeId); + final TargetCount targets = calculateTargets(targetFilter, createdAt, dsTypeId); + long totalTargets = targets.total(); + final String baseFilter = targets.filter(); if (totalTargets == 0) { throw new ConstraintDeclarationException("Rollout target filter does not match any targets"); @@ -730,4 +752,21 @@ public void triggerNextGroup(final long rolloutId) { startNextRolloutGroupAction.exec(rollout, latestRunning); } + private TargetCount calculateTargets(final String targetFilter, final Long createdAt, final Long dsTypeId) { + String baseFilter; + long totalTargets; + if (!RolloutHelper.isRolloutRetried(targetFilter)) { + baseFilter = RolloutHelper.getTargetFilterQuery(targetFilter, createdAt); + totalTargets = targetManagement.countByRsqlAndCompatible(baseFilter, dsTypeId); + } else { + totalTargets = targetManagement.countByFailedInRollout( + RolloutHelper.getIdFromRetriedTargetFilter(targetFilter), dsTypeId); + baseFilter = targetFilter; + } + + return new TargetCount(totalTargets, baseFilter); + } + + private record TargetCount(long total, String filter) {} + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java index f2b648a9aa..67dd5d10c6 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java @@ -693,6 +693,17 @@ public Slice findByTargetFilterQueryAndNotInRolloutGroupsAndCompatible(f return JpaManagementHelper.findAllWithoutCountBySpec(targetRepository, pageRequest, specList); } + @Override + public Slice findByFailedRolloutAndNotInRolloutGroups(Pageable pageRequest, Collection groups, + String rolloutId) { + final List> specList = Arrays.asList( + TargetSpecifications.failedActionsForRollout(rolloutId), + TargetSpecifications.isNotInRolloutGroups(groups) + ); + + return JpaManagementHelper.findAllWithCountBySpec(targetRepository, pageRequest, specList); + } + @Override public Slice findByInRolloutGroupWithoutAction(final Pageable pageRequest, final long group) { if (!rolloutGroupRepository.existsById(group)) { @@ -715,6 +726,15 @@ public long countByRsqlAndNotInRolloutGroupsAndCompatible(final Collection return JpaManagementHelper.countBySpec(targetRepository, specList); } + @Override + public long countByFailedRolloutAndNotInRolloutGroups(Collection groups, String rolloutId) { + final List> specList = Arrays.asList( + TargetSpecifications.failedActionsForRollout(rolloutId), + TargetSpecifications.isNotInRolloutGroups(groups)); + + return JpaManagementHelper.countBySpec(targetRepository, specList); + } + @Override public long countByRsqlAndNonDSAndCompatible(final long distributionSetId, final String targetFilterQuery) { final DistributionSet jpaDistributionSet = distributionSetManagement.getOrElseThrowException(distributionSetId); @@ -796,6 +816,14 @@ public long countByRsqlAndCompatible(final String targetFilterQuery, final Long return JpaManagementHelper.countBySpec(targetRepository, specList); } + @Override + public long countByFailedInRollout(final String rolloutId, final Long dsTypeId) { + final List> specList = List.of( + TargetSpecifications.failedActionsForRollout(rolloutId)); + + return JpaManagementHelper.countBySpec(targetRepository, specList); + } + @Override public Optional get(final long id) { return targetRepository.findById(id).map(t -> t); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java index d3d89ba2e4..7d16666fab 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java @@ -42,8 +42,10 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; import org.eclipse.hawkbit.repository.jpa.model.RolloutTargetGroup; import org.eclipse.hawkbit.repository.jpa.model.RolloutTargetGroup_; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetTag; @@ -611,4 +613,15 @@ public static Specification orderedByLinkedDistributionSet(final long }; } + public static Specification failedActionsForRollout(final String rolloutId) { + return (targetRoot, query, cb) -> { + Join targetActions = + targetRoot.join("actions"); + + return cb.and( + cb.equal(targetActions.get("rollout").get("id"), rolloutId), + cb.equal(targetActions.get("status"), Action.Status.ERROR)); + }; + } + } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java index 6de5fb3a64..3fe9355c48 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java @@ -286,7 +286,7 @@ ResponseEntity deny(@PathVariable("rolloutId") Long rolloutId, * @param representationModeParam * the representation mode parameter specifying whether a compact * or a full representation shall be returned - * + * * @return a list of all rollout groups referred to a rollout for a defined * or default page request with status OK. The response is always * paged. In any failure the JsonResponseExceptionHandler is @@ -404,4 +404,28 @@ ResponseEntity> getRolloutGroupTargets(@PathVariable("roll @PostMapping(value = MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING + "/{rolloutId}/triggerNextGroup", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) ResponseEntity triggerNextGroup(@PathVariable("rolloutId") Long rolloutId); + + /** + * Handles the POST request to retry a rollout + * + * @param rolloutId + * the ID of the rollout to be retried. + * @return OK response (200). In case of any exception the corresponding + * errors occur. + */ + @Operation(summary = "Retry a rollout", description = "Handles the POST request of retrying a rollout. Required Permission: CREATE_ROLLOUT") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))), + @ApiResponse(responseCode = "401", description = "The request requires user authentication.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be changed (i.e. read-only) or data volume restriction applies.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", description = "Rollout not found.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts and the client has to wait another second.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))) + }) + @PostMapping(value = MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING + "/{rolloutId}/retry", produces = { + MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE}) + ResponseEntity retryRollout(@PathVariable("rolloutId") final String rolloutId); + } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java index c593c6017e..7ed3d683da 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java @@ -132,6 +132,18 @@ static RolloutCreate fromRequest(final EntityFactory entityFactory, final MgmtRo .weight(restRequest.getWeight()); } + static RolloutCreate fromRetriedRollout(final EntityFactory entityFactory, final Rollout rollout) { + return entityFactory.rollout().create() + .name(rollout.getName().concat("_retry")) + .description(rollout.getDescription()) + .set(rollout.getDistributionSet()) + .targetFilterQuery("failedrollout==".concat(String.valueOf(rollout.getId()))) + .actionType(rollout.getActionType()) + .forcedTime(rollout.getForcedTime()) + .startAt(rollout.getStartAt()) + .weight(null); + } + static RolloutGroupCreate fromRequest(final EntityFactory entityFactory, final MgmtRolloutGroup restRequest) { return entityFactory.rolloutGroup().create().name(restRequest.getName()) diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java index d85a76bdcf..3048fae055 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java @@ -31,6 +31,7 @@ import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; +import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.builder.RolloutCreate; import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; @@ -39,6 +40,7 @@ import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup; +import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder; import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.security.SystemSecurityContext; @@ -47,7 +49,6 @@ import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -311,6 +312,28 @@ public ResponseEntity triggerNextGroup(@PathVariable("rolloutId") final Lo return ResponseEntity.ok().build(); } + @Override + public ResponseEntity retryRollout(final String rolloutId) { + final Rollout rolloutForRetry = this.rolloutManagement.get(Long.parseLong(rolloutId)) + .orElseThrow(EntityNotFoundException::new); + + if (rolloutForRetry.isDeleted()) { + throw new EntityNotFoundException(Rollout.class, rolloutId); + } + + if (!rolloutForRetry.getStatus().equals(Rollout.RolloutStatus.FINISHED)) { + throw new ValidationException("Rollout must be finished in order to be retried!"); + } + + final RolloutCreate create = MgmtRolloutMapper.fromRetriedRollout(entityFactory, rolloutForRetry); + final RolloutGroupConditions groupConditions = new RolloutGroupConditionBuilder().withDefaults().build(); + + final Rollout retriedRollout = rolloutManagement.create(create, 1, false, + groupConditions); + + return ResponseEntity.status(HttpStatus.CREATED).body(MgmtRolloutMapper.toResponseRollout(retriedRollout, true)); + } + private static MgmtRepresentationMode parseRepresentationMode(final String representationModeParam) { return MgmtRepresentationMode.fromValue(representationModeParam).orElseGet(() -> { // no need for a 400, just apply a safe fallback diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java index addf0f1ed0..fea2fb06b2 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java @@ -25,6 +25,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.NoSuchElementException; @@ -55,6 +56,7 @@ import org.eclipse.hawkbit.repository.test.util.WithUser; import org.eclipse.hawkbit.rest.util.JsonBuilder; import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -1577,6 +1579,99 @@ void triggeringNextGroupRolloutWrongState() throws Exception { } + @Test + @Description("Retry rollout test scenario") + public void retryRolloutTest() throws Exception { + + final DistributionSet dsA = testdataFactory.createDistributionSet(""); + final List successTargets = testdataFactory.createTargets("retryRolloutTargetSuccess-", 6); + final List failedTargets = testdataFactory.createTargets("retryRolloutTargetFailed-", 4); + + final List allTargets = new ArrayList<>(successTargets); + allTargets.addAll(failedTargets); + + postRollout("rolloutToBeRetried", 1, dsA.getId(), "id==retryRolloutTarget*", 10, Action.ActionType.FORCED); + + Rollout rollout = rolloutManagement.getByName("rolloutToBeRetried").orElseThrow(); + + // no scheduler so invoke here + rolloutHandler.handleAll(); + rolloutManagement.start(rollout.getId()); + // no scheduler so invoke here + rolloutHandler.handleAll(); + + + testdataFactory.sendUpdateActionStatusToTargets(successTargets, Status.FINISHED, "Finished successfully!"); + testdataFactory.sendUpdateActionStatusToTargets(failedTargets, Status.ERROR, "Finished error!"); + + rolloutHandler.handleAll(); + + for (Target target : allTargets) { + final List actions = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent(); + for (Action action : actions) { + if (action.getTarget().getControllerId().startsWith("retryRolloutTargetFailed")) { + Assertions.assertEquals(Status.ERROR, action.getStatus()); + } else { + Assertions.assertEquals(Status.FINISHED, action.getStatus()); + } + Assertions.assertEquals(rollout.getId(), action.getRollout().getId()); + } + } + + //retry rollout + mvc.perform(post("/rest/v1/rollouts/{rolloutId}/retry", rollout.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().is(201)); + + //search for _retried suffix + Rollout retriedRollout = rolloutManagement.getByName(rollout.getName() + "_retry").orElseThrow(); + //assert 4 targets involved + rolloutHandler.handleAll(); + + rolloutManagement.start(retriedRollout.getId()); + rolloutHandler.handleAll(); + + for (Target target : failedTargets) { + // for failed targets - check for 2 actions - one from old rollout and one from the retried + List actions = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent(); + Assertions.assertEquals(2, actions.size()); + Assertions.assertEquals(Status.ERROR, actions.get(0).getStatus()); + Assertions.assertEquals(rollout.getId(), actions.get(0).getRollout().getId()); + Assertions.assertEquals(Status.RUNNING, actions.get(1).getStatus()); + Assertions.assertEquals(retriedRollout.getId(), actions.get(1).getRollout().getId()); + } + + for (Target target : successTargets) { + //ensure no other actions from the success targets are created + List actions = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent(); + Assertions.assertEquals(1, actions.size()); + Assertions.assertEquals(rollout.getId(), actions.get(0).getRollout().getId()); + } + } + + @Test + @Description("Retrying a running rollout should not be allowed.") + public void retryNotFinishedRolloutShouldNotBeAllowed() throws Exception { + final DistributionSet dsA = testdataFactory.createDistributionSet(""); + testdataFactory.createTargets("retryRolloutTarget-", 10); + postRollout("rolloutToBeRetried", 1, dsA.getId(), "id==retryRolloutTarget*", 10, Action.ActionType.FORCED); + Rollout rollout = rolloutManagement.getByName("rolloutToBeRetried").orElseThrow(); + // no scheduler so invoke here + rolloutHandler.handleAll(); + rolloutManagement.start(rollout.getId()); + // no scheduler so invoke here + rolloutHandler.handleAll(); + + mvc.perform(post("/rest/v1/rollouts/{rolloutId}/retry", rollout.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + } + + @Test + @Description("Retrying a non-existing rollout should lead to NOT FOUND.") + public void retryNonExistingRolloutShouldLeadToNotFound() throws Exception { + mvc.perform(post("/rest/v1/rollouts/{rolloutId}/retry", 6782623)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + } + private void triggerNextGroupAndExpect(final Rollout rollout, final ResultMatcher expect) throws Exception { mvc.perform(post("/rest/v1/rollouts/{rolloutId}/triggerNextGroup", rollout.getId())) .andDo(MockMvcResultPrinter.print()).andExpect(expect); @@ -1596,6 +1691,10 @@ private void retrieveAndCompareRolloutsContent(final DistributionSet dsA, final retrieveAndCompareRolloutsContent(dsA, urlTemplate, isFullRepresentation, false, null, null); } + private Rollout getRollout(final long rolloutId) { + return rolloutManagement.get(rolloutId).orElseThrow(NoSuchElementException::new); + } + private void retrieveAndCompareRolloutsContent(final DistributionSet dsA, final String urlTemplate, final boolean isFullRepresentation, final boolean isStartTypeScheduled, final Long startAt, final Long forcetime) throws Exception { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutGrid.java index 71fab08cd9..d3362ca9c5 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutGrid.java @@ -224,6 +224,10 @@ private static boolean isCopyingAllowed(final RolloutStatus status) { return isDeletionAllowed(status) && status != RolloutStatus.CREATING; } + private static boolean isRolloutRetried(final String targetFilter) { + return targetFilter.contains("failedrollout"); + } + private static boolean isEditingAllowed(final RolloutStatus status) { final List statesThatAllowEditing = Arrays.asList(RolloutStatus.PAUSED, RolloutStatus.READY, RolloutStatus.RUNNING, RolloutStatus.STARTING, RolloutStatus.STOPPED); @@ -361,14 +365,16 @@ private void addActionColumns() { clickEvent -> updateRollout(rollout), VaadinIcons.EDIT, UIMessageIdProvider.TOOLTIP_ROLLOUT_UPDATE, SPUIStyleDefinitions.STATUS_ICON_NEUTRAL, UIComponentIdProvider.ROLLOUT_UPDATE_BUTTON_ID + "." + rollout.getId(), - permissionChecker.hasRolloutUpdatePermission() && isEditingAllowed(rollout.getStatus())); + permissionChecker.hasRolloutUpdatePermission() && isEditingAllowed(rollout.getStatus()) + && !isRolloutRetried(rollout.getTargetFilterQuery())); actionColumns.add(GridComponentBuilder.addIconColumn(this, updateButton, UPDATE_BUTTON_ID, null)); final ValueProvider copyButton = rollout -> GridComponentBuilder.buildActionButton(i18n, clickEvent -> copyRollout(rollout), VaadinIcons.COPY, UIMessageIdProvider.TOOLTIP_ROLLOUT_COPY, SPUIStyleDefinitions.STATUS_ICON_NEUTRAL, UIComponentIdProvider.ROLLOUT_COPY_BUTTON_ID + "." + rollout.getId(), - permissionChecker.hasRolloutCreatePermission() && isCopyingAllowed(rollout.getStatus())); + permissionChecker.hasRolloutCreatePermission() && isCopyingAllowed(rollout.getStatus()) + && !isRolloutRetried(rollout.getTargetFilterQuery())); actionColumns.add(GridComponentBuilder.addIconColumn(this, copyButton, COPY_BUTTON_ID, null)); actionColumns.add(GridComponentBuilder.addDeleteColumn(this, i18n, DELETE_BUTTON_ID, rolloutDeleteSupport,