Rollout retry (#1454)

* Rollout retry mechanism initial commit

Signed-off-by: Stanislav Trailov <Stanislav.Trailov@bosch.io>

* Remove test target fields for filter query

Signed-off-by: Stanislav Trailov <Stanislav.Trailov@bosch.io>

* minor refactoring

Signed-off-by: Stanislav Trailov <Stanislav.Trailov@bosch.io>

* Fixes after review

Signed-off-by: Stanislav Trailov <Stanislav.Trailov@bosch.io>

* more refactoring after review

Signed-off-by: Stanislav Trailov <Stanislav.Trailov@bosch.io>

* skip compatibility check of dstype for retried rollout

Signed-off-by: Stanislav Trailov <Stanislav.Trailov@bosch.io>

* remove dsType from javadoc

Signed-off-by: Stanislav Trailov <Stanislav.Trailov@bosch.io>

---------

Signed-off-by: Stanislav Trailov <Stanislav.Trailov@bosch.io>
This commit is contained in:
Stanislav Trailov
2023-10-19 09:58:46 +03:00
committed by GitHub
parent 22ce1c27a4
commit 44e7a72be3
11 changed files with 331 additions and 16 deletions

View File

@@ -139,6 +139,21 @@ public interface TargetManagement {
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET)
long countByRsqlAndCompatible(@NotEmpty String rsqlParam, @NotNull Long dsTypeId); 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. * Count {@link TargetFilterQuery}s for given target filter query.
* *
@@ -278,6 +293,23 @@ public interface TargetManagement {
@NotEmpty Collection<Long> groups, @NotNull String rsqlParam, @NotEmpty Collection<Long> groups, @NotNull String rsqlParam,
@NotNull DistributionSetType distributionSetType); @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<Target> findByFailedRolloutAndNotInRolloutGroups(@NotNull Pageable pageRequest,
@NotEmpty Collection<Long> groups, @NotNull String rolloutId);
/** /**
* Counts all targets for all the given parameter {@link TargetFilterQuery} * Counts all targets for all the given parameter {@link TargetFilterQuery}
* and that are not assigned to one of the {@link RolloutGroup}s and are * and that are not assigned to one of the {@link RolloutGroup}s and are
@@ -296,6 +328,20 @@ public interface TargetManagement {
long countByRsqlAndNotInRolloutGroupsAndCompatible(@NotEmpty Collection<Long> groups, @NotNull String rsqlParam, long countByRsqlAndNotInRolloutGroupsAndCompatible(@NotEmpty Collection<Long> groups, @NotNull String rsqlParam,
@NotNull DistributionSetType distributionSetType); @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<Long> groups, @NotNull String rolloutId);
/** /**
* Finds all targets of the provided {@link RolloutGroup} that have no * Finds all targets of the provided {@link RolloutGroup} that have no
* Action for the RolloutGroup. * Action for the RolloutGroup.

View File

@@ -229,6 +229,9 @@ public final class RolloutHelper {
if (StringUtils.isEmpty(group.getTargetFilterQuery())) { if (StringUtils.isEmpty(group.getTargetFilterQuery())) {
return baseFilter; return baseFilter;
} }
if (isRolloutRetried(baseFilter)) {
return baseFilter;
}
return concatAndTargetFilters(baseFilter, group.getTargetFilterQuery()); return concatAndTargetFilters(baseFilter, group.getTargetFilterQuery());
} }
@@ -253,4 +256,12 @@ public final class RolloutHelper {
+ rollout.getStatus().name().toLowerCase()); + 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());
}
} }

View File

@@ -525,10 +525,18 @@ public class JpaRolloutExecutor implements RolloutExecutor {
final List<Long> readyGroups = RolloutHelper.getGroupsByStatusIncludingGroup(rollout.getRolloutGroups(), final List<Long> readyGroups = RolloutHelper.getGroupsByStatusIncludingGroup(rollout.getRolloutGroups(),
RolloutGroupStatus.READY, group); RolloutGroupStatus.READY, group);
final long targetsInGroupFilter = DeploymentHelper.runInNewTransaction(txManager, long targetsInGroupFilter;
if (!RolloutHelper.isRolloutRetried(rollout.getTargetFilterQuery())) {
targetsInGroupFilter = DeploymentHelper.runInNewTransaction(txManager,
"countAllTargetsByTargetFilterQueryAndNotInRolloutGroups", "countAllTargetsByTargetFilterQueryAndNotInRolloutGroups",
count -> targetManagement.countByRsqlAndNotInRolloutGroupsAndCompatible(readyGroups, groupTargetFilter, 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 final long expectedInGroup = Math
.round((double) (group.getTargetPercentage() / 100) * (double) targetsInGroupFilter); .round((double) (group.getTargetPercentage() / 100) * (double) targetsInGroupFilter);
final long currentlyInGroup = DeploymentHelper.runInNewTransaction(txManager, final long currentlyInGroup = DeploymentHelper.runInNewTransaction(txManager,
@@ -572,8 +580,14 @@ public class JpaRolloutExecutor implements RolloutExecutor {
final PageRequest pageRequest = PageRequest.of(0, Math.toIntExact(limit)); final PageRequest pageRequest = PageRequest.of(0, Math.toIntExact(limit));
final List<Long> readyGroups = RolloutHelper.getGroupsByStatusIncludingGroup(rollout.getRolloutGroups(), final List<Long> readyGroups = RolloutHelper.getGroupsByStatusIncludingGroup(rollout.getRolloutGroups(),
RolloutGroupStatus.READY, group); RolloutGroupStatus.READY, group);
final Slice<Target> targets = targetManagement.findByTargetFilterQueryAndNotInRolloutGroupsAndCompatible( Slice<Target> targets;
if (!RolloutHelper.isRolloutRetried(rollout.getTargetFilterQuery())) {
targets = targetManagement.findByTargetFilterQueryAndNotInRolloutGroupsAndCompatible(
pageRequest, readyGroups, targetFilter, rollout.getDistributionSet().getType()); pageRequest, readyGroups, targetFilter, rollout.getDistributionSet().getType());
} else {
targets = targetManagement.findByFailedRolloutAndNotInRolloutGroups(
pageRequest, readyGroups, RolloutHelper.getIdFromRetriedTargetFilter(rollout.getTargetFilterQuery()));
}
createAssignmentOfTargetsToGroup(targets, group); createAssignmentOfTargetsToGroup(targets, group);

View File

@@ -196,10 +196,20 @@ public class JpaRolloutManagement implements RolloutManagement {
private JpaRollout createRollout(final JpaRollout rollout) { private JpaRollout createRollout(final JpaRollout rollout) {
WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(rollout); WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(rollout);
final Long totalTargets = targetManagement.countByRsqlAndCompatible(rollout.getTargetFilterQuery(), long totalTargets;
String errMsg;
if (RolloutHelper.isRolloutRetried(rollout.getTargetFilterQuery())) {
totalTargets = targetManagement.countByFailedInRollout(
RolloutHelper.getIdFromRetriedTargetFilter(rollout.getTargetFilterQuery()),
rollout.getDistributionSet().getType().getId()); rollout.getDistributionSet().getType().getId());
errMsg = "No failed targets in Rollout";
} else {
totalTargets = targetManagement.countByRsqlAndCompatible(rollout.getTargetFilterQuery(),
rollout.getDistributionSet().getType().getId());
errMsg = "Rollout does not match any existing targets";
}
if (totalTargets == 0) { if (totalTargets == 0) {
throw new ValidationException("Rollout does not match any existing targets"); throw new ValidationException(errMsg);
} }
rollout.setTotalTargets(totalTargets); rollout.setTotalTargets(totalTargets);
return rolloutRepository.save(rollout); return rolloutRepository.save(rollout);
@@ -618,10 +628,19 @@ public class JpaRolloutManagement implements RolloutManagement {
private RolloutGroupsValidation validateTargetsInGroups(final List<RolloutGroup> groups, final String baseFilter, private RolloutGroupsValidation validateTargetsInGroups(final List<RolloutGroup> groups, final String baseFilter,
final long totalTargets, final Long dsTypeId) { final long totalTargets, final Long dsTypeId) {
final List<Long> groupTargetCounts = new ArrayList<>(groups.size()); final List<Long> groupTargetCounts = new ArrayList<>(groups.size());
final Map<String, Long> targetFilterCounts = groups.stream() Map<String, Long> targetFilterCounts;
if (!RolloutHelper.isRolloutRetried(baseFilter)) {
targetFilterCounts = groups.stream()
.map(group -> RolloutHelper.getGroupTargetFilter(baseFilter, group)).distinct() .map(group -> RolloutHelper.getGroupTargetFilter(baseFilter, group)).distinct()
.collect(Collectors.toMap(Function.identity(), .collect(Collectors.toMap(Function.identity(),
groupTargetFilter -> targetManagement.countByRsqlAndCompatible(groupTargetFilter, dsTypeId))); groupTargetFilter -> targetManagement.countByRsqlAndCompatible(groupTargetFilter, dsTypeId)));
} else {
targetFilterCounts = groups.stream()
.map(group -> RolloutHelper.getGroupTargetFilter(baseFilter, group)).distinct()
.collect(Collectors.toMap(Function.identity(),
groupTargetFilter -> targetManagement.countByFailedInRollout(
RolloutHelper.getIdFromRetriedTargetFilter(baseFilter), dsTypeId)));
}
long unusedTargetsCount = 0; long unusedTargetsCount = 0;
@@ -675,8 +694,11 @@ public class JpaRolloutManagement implements RolloutManagement {
private long calculateRemainingTargets(final List<RolloutGroup> groups, final String targetFilter, private long calculateRemainingTargets(final List<RolloutGroup> groups, final String targetFilter,
final Long createdAt, final Long dsTypeId) { 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) { if (totalTargets == 0) {
throw new ConstraintDeclarationException("Rollout target filter does not match any targets"); throw new ConstraintDeclarationException("Rollout target filter does not match any targets");
} }
@@ -691,9 +713,9 @@ public class JpaRolloutManagement implements RolloutManagement {
public ListenableFuture<RolloutGroupsValidation> validateTargetsInGroups(final List<RolloutGroupCreate> groups, public ListenableFuture<RolloutGroupsValidation> validateTargetsInGroups(final List<RolloutGroupCreate> groups,
final String targetFilter, final Long createdAt, final Long dsTypeId) { final String targetFilter, final Long createdAt, final Long dsTypeId) {
final String baseFilter = RolloutHelper.getTargetFilterQuery(targetFilter, createdAt); final TargetCount targets = calculateTargets(targetFilter, createdAt, dsTypeId);
long totalTargets = targets.total();
final long totalTargets = targetManagement.countByRsqlAndCompatible(baseFilter, dsTypeId); final String baseFilter = targets.filter();
if (totalTargets == 0) { if (totalTargets == 0) {
throw new ConstraintDeclarationException("Rollout target filter does not match any targets"); throw new ConstraintDeclarationException("Rollout target filter does not match any targets");
@@ -730,4 +752,21 @@ public class JpaRolloutManagement implements RolloutManagement {
startNextRolloutGroupAction.exec(rollout, latestRunning); 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) {}
} }

View File

@@ -693,6 +693,17 @@ public class JpaTargetManagement implements TargetManagement {
return JpaManagementHelper.findAllWithoutCountBySpec(targetRepository, pageRequest, specList); return JpaManagementHelper.findAllWithoutCountBySpec(targetRepository, pageRequest, specList);
} }
@Override
public Slice<Target> findByFailedRolloutAndNotInRolloutGroups(Pageable pageRequest, Collection<Long> groups,
String rolloutId) {
final List<Specification<JpaTarget>> specList = Arrays.asList(
TargetSpecifications.failedActionsForRollout(rolloutId),
TargetSpecifications.isNotInRolloutGroups(groups)
);
return JpaManagementHelper.findAllWithCountBySpec(targetRepository, pageRequest, specList);
}
@Override @Override
public Slice<Target> findByInRolloutGroupWithoutAction(final Pageable pageRequest, final long group) { public Slice<Target> findByInRolloutGroupWithoutAction(final Pageable pageRequest, final long group) {
if (!rolloutGroupRepository.existsById(group)) { if (!rolloutGroupRepository.existsById(group)) {
@@ -715,6 +726,15 @@ public class JpaTargetManagement implements TargetManagement {
return JpaManagementHelper.countBySpec(targetRepository, specList); return JpaManagementHelper.countBySpec(targetRepository, specList);
} }
@Override
public long countByFailedRolloutAndNotInRolloutGroups(Collection<Long> groups, String rolloutId) {
final List<Specification<JpaTarget>> specList = Arrays.asList(
TargetSpecifications.failedActionsForRollout(rolloutId),
TargetSpecifications.isNotInRolloutGroups(groups));
return JpaManagementHelper.countBySpec(targetRepository, specList);
}
@Override @Override
public long countByRsqlAndNonDSAndCompatible(final long distributionSetId, final String targetFilterQuery) { public long countByRsqlAndNonDSAndCompatible(final long distributionSetId, final String targetFilterQuery) {
final DistributionSet jpaDistributionSet = distributionSetManagement.getOrElseThrowException(distributionSetId); final DistributionSet jpaDistributionSet = distributionSetManagement.getOrElseThrowException(distributionSetId);
@@ -796,6 +816,14 @@ public class JpaTargetManagement implements TargetManagement {
return JpaManagementHelper.countBySpec(targetRepository, specList); return JpaManagementHelper.countBySpec(targetRepository, specList);
} }
@Override
public long countByFailedInRollout(final String rolloutId, final Long dsTypeId) {
final List<Specification<JpaTarget>> specList = List.of(
TargetSpecifications.failedActionsForRollout(rolloutId));
return JpaManagementHelper.countBySpec(targetRepository, specList);
}
@Override @Override
public Optional<Target> get(final long id) { public Optional<Target> get(final long id) {
return targetRepository.findById(id).map(t -> t); return targetRepository.findById(id).map(t -> t);

View File

@@ -42,8 +42,10 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType_;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; 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.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.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetType; 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.RolloutGroup;
import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetTag; import org.eclipse.hawkbit.repository.model.TargetTag;
@@ -611,4 +613,15 @@ public final class TargetSpecifications {
}; };
} }
public static Specification<JpaTarget> failedActionsForRollout(final String rolloutId) {
return (targetRoot, query, cb) -> {
Join<JpaTarget, Action> targetActions =
targetRoot.join("actions");
return cb.and(
cb.equal(targetActions.get("rollout").get("id"), rolloutId),
cb.equal(targetActions.get("status"), Action.Status.ERROR));
};
}
} }

View File

@@ -286,7 +286,7 @@ public interface MgmtRolloutRestApi {
* @param representationModeParam * @param representationModeParam
* the representation mode parameter specifying whether a compact * the representation mode parameter specifying whether a compact
* or a full representation shall be returned * or a full representation shall be returned
* *
* @return a list of all rollout groups referred to a rollout for a defined * @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 * or default page request with status OK. The response is always
* paged. In any failure the JsonResponseExceptionHandler is * paged. In any failure the JsonResponseExceptionHandler is
@@ -404,4 +404,28 @@ public interface MgmtRolloutRestApi {
@PostMapping(value = MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING + "/{rolloutId}/triggerNextGroup", produces = { @PostMapping(value = MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING + "/{rolloutId}/triggerNextGroup", produces = {
MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE })
ResponseEntity<Void> triggerNextGroup(@PathVariable("rolloutId") Long rolloutId); ResponseEntity<Void> 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<MgmtRolloutResponseBody> retryRollout(@PathVariable("rolloutId") final String rolloutId);
} }

View File

@@ -132,6 +132,18 @@ final class MgmtRolloutMapper {
.weight(restRequest.getWeight()); .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) { static RolloutGroupCreate fromRequest(final EntityFactory entityFactory, final MgmtRolloutGroup restRequest) {
return entityFactory.rolloutGroup().create().name(restRequest.getName()) return entityFactory.rolloutGroup().create().name(restRequest.getName())

View File

@@ -31,6 +31,7 @@ import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutGroupManagement;
import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.builder.RolloutCreate; import org.eclipse.hawkbit.repository.builder.RolloutCreate;
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
@@ -39,6 +40,7 @@ import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup; 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.RolloutGroupConditions;
import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.security.SystemSecurityContext;
@@ -47,7 +49,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -311,6 +312,28 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@Override
public ResponseEntity<MgmtRolloutResponseBody> 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) { private static MgmtRepresentationMode parseRepresentationMode(final String representationModeParam) {
return MgmtRepresentationMode.fromValue(representationModeParam).orElseGet(() -> { return MgmtRepresentationMode.fromValue(representationModeParam).orElseGet(() -> {
// no need for a 400, just apply a safe fallback // no need for a 400, just apply a safe fallback

View File

@@ -25,6 +25,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
@@ -55,6 +56,7 @@ import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule;
import org.eclipse.hawkbit.repository.test.util.WithUser; import org.eclipse.hawkbit.repository.test.util.WithUser;
import org.eclipse.hawkbit.rest.util.JsonBuilder; import org.eclipse.hawkbit.rest.util.JsonBuilder;
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
@@ -1577,6 +1579,99 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
} }
@Test
@Description("Retry rollout test scenario")
public void retryRolloutTest() throws Exception {
final DistributionSet dsA = testdataFactory.createDistributionSet("");
final List<Target> successTargets = testdataFactory.createTargets("retryRolloutTargetSuccess-", 6);
final List<Target> failedTargets = testdataFactory.createTargets("retryRolloutTargetFailed-", 4);
final List<Target> 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<Action> 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<Action> 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<Action> 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 { private void triggerNextGroupAndExpect(final Rollout rollout, final ResultMatcher expect) throws Exception {
mvc.perform(post("/rest/v1/rollouts/{rolloutId}/triggerNextGroup", rollout.getId())) mvc.perform(post("/rest/v1/rollouts/{rolloutId}/triggerNextGroup", rollout.getId()))
.andDo(MockMvcResultPrinter.print()).andExpect(expect); .andDo(MockMvcResultPrinter.print()).andExpect(expect);
@@ -1596,6 +1691,10 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
retrieveAndCompareRolloutsContent(dsA, urlTemplate, isFullRepresentation, false, null, null); 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, private void retrieveAndCompareRolloutsContent(final DistributionSet dsA, final String urlTemplate,
final boolean isFullRepresentation, final boolean isStartTypeScheduled, final Long startAt, final boolean isFullRepresentation, final boolean isStartTypeScheduled, final Long startAt,
final Long forcetime) throws Exception { final Long forcetime) throws Exception {

View File

@@ -224,6 +224,10 @@ public class RolloutGrid extends AbstractGrid<ProxyRollout, String> {
return isDeletionAllowed(status) && status != RolloutStatus.CREATING; return isDeletionAllowed(status) && status != RolloutStatus.CREATING;
} }
private static boolean isRolloutRetried(final String targetFilter) {
return targetFilter.contains("failedrollout");
}
private static boolean isEditingAllowed(final RolloutStatus status) { private static boolean isEditingAllowed(final RolloutStatus status) {
final List<RolloutStatus> statesThatAllowEditing = Arrays.asList(RolloutStatus.PAUSED, RolloutStatus.READY, final List<RolloutStatus> statesThatAllowEditing = Arrays.asList(RolloutStatus.PAUSED, RolloutStatus.READY,
RolloutStatus.RUNNING, RolloutStatus.STARTING, RolloutStatus.STOPPED); RolloutStatus.RUNNING, RolloutStatus.STARTING, RolloutStatus.STOPPED);
@@ -361,14 +365,16 @@ public class RolloutGrid extends AbstractGrid<ProxyRollout, String> {
clickEvent -> updateRollout(rollout), VaadinIcons.EDIT, UIMessageIdProvider.TOOLTIP_ROLLOUT_UPDATE, clickEvent -> updateRollout(rollout), VaadinIcons.EDIT, UIMessageIdProvider.TOOLTIP_ROLLOUT_UPDATE,
SPUIStyleDefinitions.STATUS_ICON_NEUTRAL, SPUIStyleDefinitions.STATUS_ICON_NEUTRAL,
UIComponentIdProvider.ROLLOUT_UPDATE_BUTTON_ID + "." + rollout.getId(), 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)); actionColumns.add(GridComponentBuilder.addIconColumn(this, updateButton, UPDATE_BUTTON_ID, null));
final ValueProvider<ProxyRollout, Button> copyButton = rollout -> GridComponentBuilder.buildActionButton(i18n, final ValueProvider<ProxyRollout, Button> copyButton = rollout -> GridComponentBuilder.buildActionButton(i18n,
clickEvent -> copyRollout(rollout), VaadinIcons.COPY, UIMessageIdProvider.TOOLTIP_ROLLOUT_COPY, clickEvent -> copyRollout(rollout), VaadinIcons.COPY, UIMessageIdProvider.TOOLTIP_ROLLOUT_COPY,
SPUIStyleDefinitions.STATUS_ICON_NEUTRAL, SPUIStyleDefinitions.STATUS_ICON_NEUTRAL,
UIComponentIdProvider.ROLLOUT_COPY_BUTTON_ID + "." + rollout.getId(), 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.addIconColumn(this, copyButton, COPY_BUTTON_ID, null));
actionColumns.add(GridComponentBuilder.addDeleteColumn(this, i18n, DELETE_BUTTON_ID, rolloutDeleteSupport, actionColumns.add(GridComponentBuilder.addDeleteColumn(this, i18n, DELETE_BUTTON_ID, rolloutDeleteSupport,