Introduce Pause Success Action (#2867)

* Introduce Pause Success Action

Signed-off-by: vasilchev <vasil.ilchev@bosch.com>

* Instead of overriding SuccessAction, trigger next group from resume rollout
Fix Rollout Mgmt Resource to accept new Pause Action

Signed-off-by: vasilchev <vasil.ilchev@bosch.com>

* Review findings

Signed-off-by: vasilchev <vasil.ilchev@bosch.com>

* Remove unused import

---------

Signed-off-by: vasilchev <vasil.ilchev@bosch.com>
This commit is contained in:
Vasil Ilchev
2026-01-13 11:20:21 +02:00
committed by GitHub
parent c8dd5c2fe5
commit 0083d5538a
12 changed files with 299 additions and 87 deletions

View File

@@ -169,6 +169,7 @@ public interface RolloutGroup extends NamedEntity {
* is hit.
*/
enum RolloutGroupSuccessAction {
NEXTGROUP
NEXTGROUP,
PAUSE
}
}

View File

@@ -70,7 +70,8 @@ import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleRepository;
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleTypeRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetTypeRepository;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.PauseRolloutGroupAction;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.PauseRolloutGroupErrorAction;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.PauseRolloutGroupSuccessAction;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupActionEvaluator;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupConditionEvaluator;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupEvaluationManager;
@@ -213,9 +214,9 @@ public class JpaRepositoryConfiguration {
@Bean
@ConditionalOnMissingBean
PauseRolloutGroupAction pauseRolloutGroupAction(
PauseRolloutGroupErrorAction pauseRolloutGroupErrorAction(
final RolloutManagement rolloutManagement, final RolloutGroupRepository rolloutGroupRepository) {
return new PauseRolloutGroupAction(rolloutManagement, rolloutGroupRepository);
return new PauseRolloutGroupErrorAction(rolloutManagement, rolloutGroupRepository);
}
@Bean
@@ -225,6 +226,13 @@ public class JpaRepositoryConfiguration {
return new StartNextGroupRolloutGroupSuccessAction(rolloutGroupRepository, deploymentManagement);
}
@Bean
@ConditionalOnMissingBean
PauseRolloutGroupSuccessAction pauseRolloutGroupSuccessAction(final RolloutManagement rolloutManagement,
final RolloutGroupRepository rolloutGroupRepository) {
return new PauseRolloutGroupSuccessAction(rolloutManagement, rolloutGroupRepository);
}
@Bean
@ConditionalOnMissingBean
ThresholdRolloutGroupErrorCondition thresholdRolloutGroupErrorCondition(final ActionRepository actionRepository) {

View File

@@ -66,6 +66,7 @@ import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetRepository;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupEvaluationManager;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRolloutGroupSuccessAction;
import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications;
import org.eclipse.hawkbit.repository.jpa.specifications.RolloutSpecification;
@@ -86,8 +87,10 @@ import org.eclipse.hawkbit.repository.model.TotalTargetCountActionStatus;
import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus;
import org.eclipse.hawkbit.repository.qfields.RolloutFields;
import org.eclipse.hawkbit.utils.ObjectCopyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.context.annotation.Lazy;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -118,6 +121,9 @@ public class JpaRolloutManagement implements RolloutManagement {
RolloutStatus.CREATING, RolloutStatus.READY, RolloutStatus.WAITING_FOR_APPROVAL, RolloutStatus.STARTING, RolloutStatus.RUNNING,
RolloutStatus.PAUSED, RolloutStatus.APPROVAL_DENIED);
private static final Comparator<RolloutGroup> ROLLOUT_GROUP_DESC_COMP = Comparator.comparingLong(RolloutGroup::getId).reversed();
private RolloutGroupEvaluationManager rolloutGroupEvaluationManager;
@Value("${hawkbit.repository.jpa.management.rollout.max.actions.per.transaction:5000}")
private int maxActions;
@@ -164,6 +170,13 @@ public class JpaRolloutManagement implements RolloutManagement {
quotaManagement, this::isMultiAssignmentsEnabled, this::isConfirmationFlowEnabled, repositoryProperties, null);
}
@Autowired
@Lazy
private void setRolloutGroupEvaluationManager(
final RolloutGroupEvaluationManager rolloutGroupEvaluationManager) {
this.rolloutGroupEvaluationManager = rolloutGroupEvaluationManager;
}
public static String createRolloutLockKey(final String tenant) {
return tenant + "-rollout";
}
@@ -348,10 +361,34 @@ public class JpaRolloutManagement implements RolloutManagement {
throw new RolloutIllegalStateException("Rollout can only be resumed in state paused but current state is " +
rollout.getStatus().name().toLowerCase());
}
final List<RolloutGroup> allStartedGroups = rollout.getRolloutGroups().stream()
.filter(g -> RolloutGroupStatus.SCHEDULED != g.getStatus()).toList();
if (!allStartedGroups.isEmpty()) {
final RolloutGroup lastStartedGroup = allStartedGroups.get(allStartedGroups.size() - 1);
if (shouldStartNextGroupOnResume(rollout, lastStartedGroup)) {
startNextRolloutGroupAction.exec(rollout, lastStartedGroup);
}
}
rollout.setStatus(RolloutStatus.RUNNING);
rolloutRepository.save(rollout);
}
/**
* Check if on resume of a paused rollout the next group shall be started directly.
* Cases where we need to manually start the next group:
* - last running group is in error state and there is still some old group in running state, only running groups would be evaluated which would leave Rollout in running state but no trigger new group
* - last running group has success action to PAUSE and the success condition is fulfilled
* @param rollout
* @param lastStartedGroup
* @return true if next group shall be started directly on resume, false otherwise
*/
private boolean shouldStartNextGroupOnResume(final JpaRollout rollout, final RolloutGroup lastStartedGroup) {
return lastStartedGroup.getStatus().equals(RolloutGroupStatus.ERROR) ||
(lastStartedGroup.getSuccessAction() == RolloutGroup.RolloutGroupSuccessAction.PAUSE &&
rolloutGroupEvaluationManager.getSuccessConditionEvaluator(lastStartedGroup.getSuccessCondition())
.eval(rollout, lastStartedGroup, lastStartedGroup.getSuccessConditionExp()));
}
@Override
@Transactional
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
@@ -482,13 +519,15 @@ public class JpaRolloutManagement implements RolloutManagement {
throw new RolloutIllegalStateException("Rollout does not have any groups left to be triggered");
}
final RolloutGroup latestRunning = groups.stream()
.sorted(Comparator.comparingLong(RolloutGroup::getId).reversed())
.filter(g -> RolloutGroupStatus.RUNNING.equals(g.getStatus()))
.findFirst()
.orElseThrow(() -> new RolloutIllegalStateException("No group is running"));
startNextRolloutGroupAction.exec(rollout, latestRunning);
final List<JpaRolloutGroup> startedRolloutGroups = rollout.getRolloutGroups().stream()
.filter(group -> group.getStatus() != RolloutGroupStatus.SCHEDULED)
.sorted(ROLLOUT_GROUP_DESC_COMP)
.map(JpaRolloutGroup.class::cast)
.toList();
if (startedRolloutGroups.isEmpty()) {
throw new RolloutIllegalStateException("Cannot find any started rollout group to trigger next from");
}
startNextRolloutGroupAction.exec(rollout, startedRolloutGroups.get(0));
}
@Override

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -12,38 +12,27 @@ package org.eclipse.hawkbit.repository.jpa.rollout.condition;
import static org.eclipse.hawkbit.context.AccessContext.asSystem;
import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus;
/**
* Error action evaluator which pauses the whole {@link Rollout} and sets the
* current {@link RolloutGroup} to error.
* Abstract class for pausing a rollout group.
*
* @param <T> the type of the action
*/
public class PauseRolloutGroupAction implements RolloutGroupActionEvaluator<RolloutGroup.RolloutGroupErrorAction> {
public abstract class AbstractPauseRolloutGroupAction<T extends Enum<T>> implements RolloutGroupActionEvaluator<T> {
private final RolloutManagement rolloutManagement;
private final RolloutGroupRepository rolloutGroupRepository;
protected final RolloutManagement rolloutManagement;
protected final RolloutGroupRepository rolloutGroupRepository;
public PauseRolloutGroupAction(final RolloutManagement rolloutManagement,
protected AbstractPauseRolloutGroupAction(final RolloutManagement rolloutManagement,
final RolloutGroupRepository rolloutGroupRepository) {
this.rolloutManagement = rolloutManagement;
this.rolloutGroupRepository = rolloutGroupRepository;
}
@Override
public RolloutGroup.RolloutGroupErrorAction getAction() {
return RolloutGroup.RolloutGroupErrorAction.PAUSE;
}
@Override
public void exec(final Rollout rollout, final RolloutGroup rolloutG) {
final JpaRolloutGroup rolloutGroup = (JpaRolloutGroup) rolloutG;
rolloutGroup.setStatus(RolloutGroupStatus.ERROR);
rolloutGroupRepository.save(rolloutGroup);
public void exec(final Rollout rollout, final RolloutGroup rolloutGroup) {
// Refresh latest rollout state in order to avoid cases when
// previous group have matched error condition and paused the rollout
// and this one tries to pause the rollout too but throws an exception
@@ -56,3 +45,4 @@ public class PauseRolloutGroupAction implements RolloutGroupActionEvaluator<Roll
}
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rollout.condition;
import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus;
/**
* Error action evaluator which pauses the whole {@link Rollout} and sets the
* current {@link RolloutGroup} to error.
*/
public class PauseRolloutGroupErrorAction extends AbstractPauseRolloutGroupAction<RolloutGroup.RolloutGroupErrorAction> {
public PauseRolloutGroupErrorAction(final RolloutManagement rolloutManagement,
final RolloutGroupRepository rolloutGroupRepository) {
super(rolloutManagement, rolloutGroupRepository);
}
@Override
public RolloutGroup.RolloutGroupErrorAction getAction() {
return RolloutGroup.RolloutGroupErrorAction.PAUSE;
}
@Override
public void exec(final Rollout rollout, final RolloutGroup rolloutG) {
// set rollout group status to error
final JpaRolloutGroup rolloutGroup = (JpaRolloutGroup) rolloutG;
rolloutGroup.setStatus(RolloutGroupStatus.ERROR);
rolloutGroupRepository.save(rolloutGroup);
// pause the rollout
super.exec(rollout, rolloutGroup);
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rollout.condition;
import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
/**
* Success action evaluator which pauses the whole {@link Rollout}.
*/
public class PauseRolloutGroupSuccessAction
extends AbstractPauseRolloutGroupAction<RolloutGroup.RolloutGroupSuccessAction> {
public PauseRolloutGroupSuccessAction(final RolloutManagement rolloutManagement,
final RolloutGroupRepository rolloutGroupRepository) {
super(rolloutManagement, rolloutGroupRepository);
}
@Override
public RolloutGroup.RolloutGroupSuccessAction getAction() {
return RolloutGroup.RolloutGroupSuccessAction.PAUSE;
}
@Override
public void exec(final Rollout rollout, final RolloutGroup rolloutGroup) {
if (!rolloutGroupRepository
.findByParentIdAndStatus(rolloutGroup.getId(), RolloutGroup.RolloutGroupStatus.SCHEDULED).isEmpty()) {
// if there are still scheduled child groups, do pause the rollout, otherwise just let it in running state
super.exec(rollout, rolloutGroup);
}
}
}

View File

@@ -111,7 +111,6 @@ public class JpaRolloutExecutor implements RolloutExecutor {
*/
private static final List<Status> DOWNLOAD_ONLY_ACTION_TERMINATION_STATUSES =
List.of(Status.ERROR, Status.FINISHED, Status.CANCELED, Status.DOWNLOADED);
private static final Comparator<RolloutGroup> DESC_COMP = Comparator.comparingLong(RolloutGroup::getId).reversed();
private static final String TRANSACTION_ASSIGNING_TARGETS_TO_ROLLOUT_GROUP_FAILED = "Transaction assigning Targets to RolloutGroup failed";
private final ActionRepository actionRepository;
@@ -353,11 +352,10 @@ public class JpaRolloutExecutor implements RolloutExecutor {
.filter(group -> group.getStatus() == RolloutGroupStatus.RUNNING)
.map(JpaRolloutGroup.class::cast)
.toList();
if (runningGroups.isEmpty()) {
// no running rollouts, probably there was an error somewhere at the latest group. And the latest group has
// been switched from running into error state. So we need to find the latest group which
executeLatestRolloutGroup(rollout);
asSystem(() -> rolloutManagement.triggerNextGroup(rollout.getId()));
} else {
log.debug("Rollout {} has {} running groups", rollout.getId(), runningGroups.size());
executeRunningGroups(rollout, runningGroups, rollout.getRolloutGroups().get(rollout.getRolloutGroups().size() - 1));
@@ -410,18 +408,6 @@ public class JpaRolloutExecutor implements RolloutExecutor {
return groupsActiveLeft == 0;
}
private void executeLatestRolloutGroup(final JpaRollout rollout) {
final List<JpaRolloutGroup> latestRolloutGroup = rollout.getRolloutGroups().stream()
.filter(group -> group.getStatus() != RolloutGroupStatus.SCHEDULED)
.sorted(DESC_COMP)
.map(JpaRolloutGroup.class::cast)
.toList();
if (latestRolloutGroup.isEmpty()) {
return;
}
executeRolloutGroupSuccessAction(rollout, latestRolloutGroup.get(0));
}
// fakes getTotalTargets count to match expected for the last dynamic group
// so the evaluation to use total targets to properly
private RolloutGroup evalProxy(final RolloutGroup group) {
@@ -530,7 +516,7 @@ public class JpaRolloutExecutor implements RolloutExecutor {
.eval(rollout, evalProxy, rolloutGroup.getSuccessConditionExp());
if (isFinished) {
log.debug("Rollout group {} is finished, starting next group", rolloutGroup);
executeRolloutGroupSuccessAction(rollout, rolloutGroup);
evaluationManager.getSuccessActionEvaluator(rolloutGroup.getSuccessAction()).exec(rollout, rolloutGroup);
} else {
log.debug("Rollout group {} is still running", rolloutGroup);
}
@@ -539,10 +525,6 @@ public class JpaRolloutExecutor implements RolloutExecutor {
}
}
private void executeRolloutGroupSuccessAction(final Rollout rollout, final RolloutGroup rolloutGroup) {
evaluationManager.getSuccessActionEvaluator(rolloutGroup.getSuccessAction()).exec(rollout, rolloutGroup);
}
private void startFirstRolloutGroup(final JpaRollout rollout) {
log.debug("startFirstRolloutGroup called for rollout {}", rollout.getId());

View File

@@ -27,6 +27,7 @@ import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus;
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessAction;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Sort;
@@ -52,7 +53,21 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
*/
@SneakyThrows
@Test
void rolloutFlow() {
void rolloutDefaultFlow() {
rolloutFlow(RolloutGroupSuccessAction.NEXTGROUP);
}
/**
* Verifies a simple rollout flow
*/
@SneakyThrows
@Test
void rolloutPauseFlow() {
rolloutFlow(RolloutGroupSuccessAction.PAUSE);
}
void rolloutFlow(final RolloutGroupSuccessAction successAction) throws Exception {
final String rolloutName = "rollout-std";
final int amountGroups = 5; // static only
final String targetPrefix = "controller-rollout-std-";
@@ -62,7 +77,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
final Rollout rollout = callAs(
withUser("rolloutFlowUser", "READ_DISTRIBUTION_SET", "READ_TARGET", "READ_ROLLOUT", "CREATE_ROLLOUT"),
() -> testdataFactory.createRolloutByVariables(rolloutName, rolloutName, amountGroups,
"controllerid==" + targetPrefix + "*", distributionSet, "60", "30", false, false));
"controllerid==" + targetPrefix + "*", distributionSet, "60", successAction,"30", false, false));
final List<RolloutGroup> groups = rolloutGroupManagement.findByRollout(
rollout.getId(), new OffsetBasedPageRequest(0, amountGroups + 10, Sort.by(Direction.ASC, "id"))).getContent();
@@ -78,7 +93,9 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
assertGroup(groups.get(i), false, i == 0 ? RolloutGroupStatus.RUNNING : RolloutGroupStatus.SCHEDULED, 3);
}
executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(groups, rollout, amountGroups);
executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(groups, rollout, amountGroups, successAction);
assertRollout(rollout, false, RolloutStatus.RUNNING, amountGroups, amountGroups * 3);
rolloutManagement.pauseRollout(rollout.getId());
rolloutHandler.handleAll();
@@ -92,11 +109,24 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
}
/**
* Verifies a simple dynamic rollout flow
* Verifies a simple dynamic rollout flow with default {@link RolloutGroupSuccessAction#NEXTGROUP} success action
*/
@SneakyThrows
@Test
void dynamicRolloutFlow() {
void dynamicRolloutDefaultFlow() {
dynamicRolloutFlow(RolloutGroupSuccessAction.NEXTGROUP);
}
/**
* Verifies a simple dynamic rollout flow with {@link RolloutGroupSuccessAction#PAUSE} success action
*/
@SneakyThrows
@Test
void dynamicRolloutPauseFlow() {
dynamicRolloutFlow(RolloutGroupSuccessAction.PAUSE);
}
void dynamicRolloutFlow(final RolloutGroup.RolloutGroupSuccessAction successAction) throws Exception {
final String rolloutName = "dynamic-rollout-std";
final int amountGroups = 2; // static only
final String targetPrefix = "controller-dynamic-rollout-std-";
@@ -106,7 +136,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
final Rollout rollout = callAs(
withUser("dynamicRolloutFlow", "READ_DISTRIBUTION_SET", "READ_TARGET", "READ_ROLLOUT", "CREATE_ROLLOUT"),
() -> testdataFactory.createRolloutByVariables(rolloutName, rolloutName, amountGroups,
"controllerid==" + targetPrefix + "*", distributionSet, "60", "30", false, true));
"controllerid==" + targetPrefix + "*", distributionSet, "60", successAction,"30", false, true));
// rollout is READY
assertRollout(rollout, true, RolloutStatus.READY, amountGroups + 1, amountGroups * 3);
@@ -132,7 +162,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
}
assertGroup(dynamic1, true, RolloutGroupStatus.SCHEDULED, 0);
executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(groups, rollout, amountGroups);
executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(groups, rollout, amountGroups, successAction);
// partially fill the first dynamic (it is running and now create actions for 2 targets)
rolloutHandler.handleAll();
@@ -166,7 +196,13 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
.forEach(this::finishAction);
executeWithoutOneTargetFromAGroup(dynamic1, rollout, 3);
assertAndGetRunning(rollout, 1); // remains on in the first dynamic
if (successAction == RolloutGroupSuccessAction.PAUSE) {
// let success pause action run
rolloutHandler.handleAll();
// external resume rollout
rolloutManagement.resumeRollout(rollout.getId());
}
// start next group
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 4);
assertGroup(groups.get(amountGroups - 1), false, RolloutGroupStatus.FINISHED, 3);
@@ -208,12 +244,26 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
assertThat(refresh(dynamic2).getStatus()).isEqualTo(RolloutGroupStatus.FINISHED);
}
/**
* Verifies a simple dynamic rollout flow with a dynamic group template
* Verifies a simple dynamic rollout flow with a dynamic group template with default {@link RolloutGroupSuccessAction#NEXTGROUP} success action
*/
@SneakyThrows
@Test
void dynamicRolloutTemplateFlow() {
void dynamicDefaultRolloutTemplateFlow() {
dynamicRolloutTemplateFlow(RolloutGroup.RolloutGroupSuccessAction.NEXTGROUP);
}
/**
* Verifies a simple dynamic rollout flow with a dynamic group template with {@link RolloutGroupSuccessAction#PAUSE} success action
*/
@SneakyThrows
@Test
void dynamicPauseRolloutTemplateFlow() {
dynamicRolloutTemplateFlow(RolloutGroup.RolloutGroupSuccessAction.PAUSE);
}
void dynamicRolloutTemplateFlow(final RolloutGroup.RolloutGroupSuccessAction successAction) throws Exception {
final String rolloutName = "dynamic-template-rollout-std";
final int amountGroups = 3; // static only
final String targetPrefix = "controller-template-dynamic-rollout-std-";
@@ -224,7 +274,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
final Rollout rollout = callAs(
withUser("dynamicRolloutTemplateFlow", "READ_DISTRIBUTION_SET", "READ_TARGET", "READ_ROLLOUT", "CREATE_ROLLOUT"),
() -> testdataFactory.createRolloutByVariables(rolloutName, rolloutName, amountGroups,
"controllerid==" + targetPrefix + "*", distributionSet, "60", "30",
"controllerid==" + targetPrefix + "*", distributionSet, "60", successAction, "30",
Action.ActionType.FORCED, 1000, false, true,
RolloutManagement.DynamicRolloutGroupTemplate.builder().nameSuffix("-dyn").targetCount(6).build()));
@@ -252,7 +302,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
}
assertGroup(dynamic1, true, RolloutGroupStatus.SCHEDULED, 0);
executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(groups, rollout, amountGroups);
executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(groups, rollout, amountGroups, successAction);
// partially fill the first dynamic (it is running and now create actions for 4 targets)
rolloutHandler.handleAll();
@@ -287,6 +337,13 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
executeWithoutOneTargetFromAGroup(dynamic1, rollout, 6);
assertAndGetRunning(rollout, 1); // remains on in the first dynamic
if (successAction == RolloutGroupSuccessAction.PAUSE) {
// let success pause action run
rolloutHandler.handleAll();
// external resume rollout
rolloutManagement.resumeRollout(rollout.getId());
}
// start next group
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 8);
assertGroup(groups.get(amountGroups - 1), false, RolloutGroupStatus.FINISHED, 3);
@@ -310,11 +367,24 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
}
/**
* Verifies a simple pure (no static groups) dynamic rollout flow with a dynamic group template
* Verifies a simple pure (no static groups) dynamic rollout flow with a dynamic group template with default {@link RolloutGroupSuccessAction#NEXTGROUP} success action
*/
@SneakyThrows
@Test
void dynamicRolloutPureFlow() {
void dynamicDefaultRolloutPureFlow() {
dynamicRolloutPureFlow(RolloutGroup.RolloutGroupSuccessAction.NEXTGROUP);
}
/**
* Verifies a simple pure (no static groups) dynamic rollout flow with a dynamic group template with {@link RolloutGroupSuccessAction#PAUSE} success action
*/
@SneakyThrows
@Test
void dynamicPauseRolloutPureFlow() {
dynamicRolloutPureFlow(RolloutGroup.RolloutGroupSuccessAction.PAUSE);
}
void dynamicRolloutPureFlow(final RolloutGroup.RolloutGroupSuccessAction successAction) throws Exception {
final String rolloutName = "pure-dynamic-rollout-std";
final String targetPrefix = "controller-pure-dynamic-rollout-std-";
final DistributionSet distributionSet = testdataFactory.createDistributionSetLocked("dsFor" + rolloutName);
@@ -322,7 +392,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
final Rollout rollout = callAs(
withUser("dynamicRolloutPureFlow", "READ_DISTRIBUTION_SET", "READ_TARGET", "READ_ROLLOUT", "CREATE_ROLLOUT"),
() -> testdataFactory.createRolloutByVariables(rolloutName, rolloutName, 0,
"controllerid==" + targetPrefix + "*", distributionSet, "60", "30",
"controllerid==" + targetPrefix + "*", distributionSet, "60", successAction,"30",
Action.ActionType.FORCED, 1000, false, true,
RolloutManagement.DynamicRolloutGroupTemplate.builder().nameSuffix("-dyn").targetCount(6).build()));
@@ -373,6 +443,13 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
executeWithoutOneTargetFromAGroup(dynamic1, rollout, 6);
assertAndGetRunning(rollout, 1); // remains on in the first dynamic
if (successAction == RolloutGroupSuccessAction.PAUSE) {
// let success pause action run
rolloutHandler.handleAll();
// external resume rollout
rolloutManagement.resumeRollout(rollout.getId());
}
// start next group
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.RUNNING, 2, 8);
assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 6);
@@ -395,11 +472,24 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
}
/**
* Verifies a simple rollout flow
* Verifies a simple rollout flow with {@link RolloutGroupSuccessAction#NEXTGROUP} success action
*/
@SneakyThrows
@Test
void rollout0ThresholdFlow() {
void rolloutDefault0ThresholdFlow() {
rollout0ThresholdFlow(RolloutGroupSuccessAction.NEXTGROUP);
}
/**
* Verifies a simple rollout flow with {@link RolloutGroupSuccessAction#PAUSE} success action
*/
@SneakyThrows
@Test
void rolloutPause0ThresholdFlow() {
rollout0ThresholdFlow(RolloutGroupSuccessAction.PAUSE);
}
void rollout0ThresholdFlow(final RolloutGroup.RolloutGroupSuccessAction successAction) throws Exception {
final String rolloutName = "rollout-std-0threshold";
final int amountGroups = 5; // static only
final String targetPrefix = "controller-rollout-std-0threshold-";
@@ -409,7 +499,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
final Rollout rollout = callAs(
withUser("rollout0ThresholdFlow", "READ_DISTRIBUTION_SET", "READ_TARGET", "READ_ROLLOUT", "CREATE_ROLLOUT"),
() -> testdataFactory.createRolloutByVariables(rolloutName, rolloutName, amountGroups,
"controllerid==" + targetPrefix + "*", distributionSet, "0", "25", false, false));
"controllerid==" + targetPrefix + "*", distributionSet, "0", successAction, "25", false, false));
final List<RolloutGroup> groups = rolloutGroupManagement.findByRollout(
rollout.getId(), new OffsetBasedPageRequest(0, amountGroups + 10, Sort.by(Direction.ASC, "id"))).getContent();
@@ -423,6 +513,12 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
for (int i = 0; i < amountGroups; i++) {
assertGroup(groups.get(i), false, i < step ? RolloutGroupStatus.RUNNING : RolloutGroupStatus.SCHEDULED, 3);
}
// expect all groups without last to trigger PAUSE action
if (step < amountGroups && successAction == RolloutGroupSuccessAction.PAUSE) {
rolloutHandler.handleAll();
rolloutManagement.resumeRollout(rollout.getId());
}
// starting the next group
rolloutHandler.handleAll();
}
@@ -430,7 +526,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
private void executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(
final List<RolloutGroup> groups,
final Rollout rollout, final int amountGroups) {
final Rollout rollout, final int amountGroups, final RolloutGroupSuccessAction successAction) {
// create dynamic group if needed
rolloutHandler.handleAll();
// execute groups (without on of the last)
@@ -455,6 +551,10 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
.forEach(this::finishAction);
assertAndGetRunning(rollout, i + 1 == amountGroups ? 1 : 0);
rolloutHandler.handleAll();
if (i + 1 < groups.size() && successAction == RolloutGroupSuccessAction.PAUSE) {
rolloutManagement.resumeRollout(rollout.getId());
rolloutHandler.handleAll();
}
final RolloutGroupStatus expectedStatus =
i + 1 == amountGroups ? RolloutGroupStatus.RUNNING : RolloutGroupStatus.FINISHED;
assertThat(refresh(groups.get(i)).getStatus())

View File

@@ -117,7 +117,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
testdataFactory.createTargets(targetPrefix, 0, amountGroups * 2);
final Rollout dynamicRollout = testdataFactory.createRolloutByVariables("dynamic", "static rollout", amountGroups,
"controllerid==" + targetPrefix + "*", distributionSet, "0", "30", ActionType.FORCED, 1000, false, true);
"controllerid==" + targetPrefix + "*", distributionSet, "0", RolloutGroup.RolloutGroupSuccessAction.NEXTGROUP, "30", ActionType.FORCED, 1000, false, true);
rolloutManagement.start(dynamicRollout.getId());
rolloutHandler.handleAll();
assertRollout(dynamicRollout, true, RolloutStatus.RUNNING, amountGroups + 1, amountGroups * 2);
@@ -152,7 +152,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
testdataFactory.createTargets(targetPrefix, amountGroups * 2, amountGroups);
final Rollout staticRollout = testdataFactory.createRolloutByVariables("static", "static rollout", amountGroups,
"controllerid==" + targetPrefix + "*", distributionSet, "0", "30", ActionType.FORCED, 0, false, false);
"controllerid==" + targetPrefix + "*", distributionSet, "0", RolloutGroup.RolloutGroupSuccessAction.NEXTGROUP,"30", ActionType.FORCED, 0, false, false);
rolloutManagement.start(staticRollout.getId());
rolloutHandler.handleAll();
assertRollout(staticRollout, false, RolloutStatus.RUNNING, amountGroups, amountGroups * 3);

View File

@@ -63,6 +63,7 @@ import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.NamedVersionedEntity;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupErrorAction;
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupErrorCondition;
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCondition;
@@ -982,10 +983,10 @@ public class TestdataFactory {
public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription,
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
final String successCondition, final String errorCondition, final boolean confirmationRequired,
final String successCondition, final RolloutGroup.RolloutGroupSuccessAction successAction, final String errorCondition, final boolean confirmationRequired,
final boolean dynamic) {
return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet,
successCondition, errorCondition, Action.ActionType.FORCED, null, confirmationRequired, dynamic);
successCondition, successAction, errorCondition, Action.ActionType.FORCED, null, confirmationRequired, dynamic);
}
public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription,
@@ -993,7 +994,7 @@ public class TestdataFactory {
final String successCondition, final String errorCondition, final Action.ActionType actionType,
final Integer weight, final boolean confirmationRequired) {
return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet,
successCondition, errorCondition, actionType, weight, confirmationRequired, false);
successCondition, RolloutGroup.RolloutGroupSuccessAction.NEXTGROUP, errorCondition, actionType, weight, confirmationRequired, false);
}
/**
@@ -1014,19 +1015,21 @@ public class TestdataFactory {
*/
public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription,
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
final String successCondition, final String errorCondition, final Action.ActionType actionType,
final String successCondition, final RolloutGroup.RolloutGroupSuccessAction successAction, final String errorCondition, final Action.ActionType actionType,
final Integer weight, final boolean confirmationRequired, final boolean dynamic) {
return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet,
successCondition, errorCondition, actionType, weight, confirmationRequired, dynamic, null);
successCondition, successAction, errorCondition, actionType, weight, confirmationRequired, dynamic, null);
}
public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription,
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
final String successCondition, final String errorCondition, final Action.ActionType actionType,
final String successCondition, final RolloutGroup.RolloutGroupSuccessAction successAction, final String errorCondition,
final Action.ActionType actionType,
final Integer weight, final boolean confirmationRequired, final boolean dynamic,
final RolloutManagement.DynamicRolloutGroupTemplate dynamicRolloutGroupTemplate) {
final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition)
.successAction(successAction, "")
.errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition)
.errorAction(RolloutGroupErrorAction.PAUSE, null).build();