From 7768e543fdd987c5c0f781babeabe39c8fa1f39f Mon Sep 17 00:00:00 2001 From: Avgustin Marinov Date: Thu, 18 Jan 2024 11:37:01 +0200 Subject: [PATCH] [#1548] Add support for dynamic rollouts (#1533) * [#1548] Add support for dynamic rollouts -- Current status -- Initial draft only !!!, to be improved TODO: * evaluate the target count - if update group/rollout total count fails dynamic updates could (?), actually, contain more targets * is it needed to break handler on group creating? * if dynamic group schedulers occur to be heavy - maybe a handler per tenant will ensure that one tenant won't break all *Concept for dynamic groups*: Rollouts are static and dynamic. Static rollouts consist of static groups only while dynamic rollouts have a number of static groups (first groups) and then an unlimited number of dynamic groups. Group targets assignments: * static groups include ALL matching targets created at the time the rollout was created, nevertheless they have active actions with bigger weight or not. Actions for the rollout and included targeets however are created at the start time. * dynamic groups however are filled in when started and consider the action weight. The targets included in a dynamic group are: * matching (filter and distribution set compatible) * not included in this or following rollout static groups (if already included in any of the following rollouts - it's intended to be overridden) * not in active actions of any rollouts with equal or bigger weight In general, when you create a rollout it contains all matching targets available at create time overriding any previous rollouts, actions, and so on. If the rollout is dynamic when its dynamic group becomes running it gets only matching targets that doesn't belong to static groups or have actions with great or equal weight Signed-off-by: Marinov Avgustin * [#1548] Add 1000 weight for actions, rollouts and auto assignments without weight Signed-off-by: Marinov Avgustin --------- Signed-off-by: Marinov Avgustin --- .../hawkbit/repository/TargetManagement.java | 12 +- .../repository/builder/RolloutCreate.java | 10 +- .../hawkbit/repository/model/Rollout.java | 5 + .../repository/model/RolloutGroup.java | 5 + .../hawkbit/repository/RolloutHelper.java | 21 +- .../repository/jpa/JpaRolloutExecutor.java | 313 ++++++++++++++---- .../RepositoryApplicationConfiguration.java | 3 +- .../jpa/builder/JpaRolloutCreate.java | 7 + .../AbstractDsAssignmentStrategy.java | 9 +- .../management/JpaDeploymentManagement.java | 4 +- .../jpa/management/JpaRolloutManagement.java | 26 +- .../JpaTargetFilterQueryManagement.java | 14 +- .../jpa/management/JpaTargetManagement.java | 21 ++ .../OfflineDsAssignmentStrategy.java | 5 +- .../OnlineDsAssignmentStrategy.java | 5 +- .../repository/jpa/model/JpaRollout.java | 18 + .../repository/jpa/model/JpaRolloutGroup.java | 12 + .../jpa/model/JpaTargetFilterQuery.java | 2 +- ...artNextGroupRolloutGroupSuccessAction.java | 14 +- .../ThresholdRolloutGroupErrorCondition.java | 3 +- ...ThresholdRolloutGroupSuccessCondition.java | 1 - .../specifications/TargetSpecifications.java | 55 ++- .../jpa/utils/WeightValidationHelper.java | 4 +- .../V1_12_28__add_dynamic_rollout___DB2.sql | 9 + .../H2/V1_12_28__add_dynamic_rollout___H2.sql | 9 + .../V1_12_28__add_dynamic_rollout___MYSQL.sql | 9 + ...2_28__add_dynamic_rollout___POSTGRESQL.sql | 9 + ...2_28__add_dynamic_rollout___SQL_SERVER.sql | 9 + .../remote/RemoteTenantAwareEventTest.java | 2 + .../event/remote/entity/ActionEventTest.java | 2 + .../jpa/AbstractJpaIntegrationTest.java | 45 +++ .../autoassign/AutoAssignCheckerIntTest.java | 2 +- .../autocleanup/AutoActionCleanupTest.java | 1 + .../management/ControllerManagementTest.java | 3 + .../management/DeploymentManagementTest.java | 8 +- .../management/RolloutManagementFlowTest.java | 205 ++++++++++++ .../jpa/management/RolloutManagementTest.java | 97 ++++-- .../TargetFilterQueryManagementTest.java | 12 +- .../jpa/management/TargetManagementTest.java | 63 ++++ .../test/util/AbstractIntegrationTest.java | 11 + .../repository/test/util/TestdataFactory.java | 32 +- .../rollout/MgmtRolloutResponseBody.java | 12 + .../rollout/MgmtRolloutRestRequestBody.java | 19 ++ .../mgmt/rest/resource/MgmtRolloutMapper.java | 2 + .../MgmtDistributionSetResourceTest.java | 5 +- .../resource/MgmtRolloutResourceTest.java | 13 +- .../MgmtTargetFilterQueryResourceTest.java | 3 +- .../rest/resource/MgmtTargetResourceTest.java | 28 +- hawkbit-sdk/hawkbit-sdk-test/spring-shell.log | 1 - pom.xml | 2 +- 50 files changed, 1005 insertions(+), 177 deletions(-) create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_28__add_dynamic_rollout___DB2.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_28__add_dynamic_rollout___H2.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_28__add_dynamic_rollout___MYSQL.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_28__add_dynamic_rollout___POSTGRESQL.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_28__add_dynamic_rollout___SQL_SERVER.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementFlowTest.java delete mode 100644 hawkbit-sdk/hawkbit-sdk-test/spring-shell.log 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 ecde4b63d..7b143b2fb 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 @@ -310,7 +310,7 @@ public interface TargetManagement { * the pageRequest to enhance the query for paging and sorting * @param groups * the list of {@link RolloutGroup}s - * @param rsqlParam + * @param targetFilterQuery * filter definition in RSQL syntax * @param distributionSetType * type of the {@link DistributionSet} the targets must be compatible @@ -319,9 +319,17 @@ public interface TargetManagement { */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) Slice findByTargetFilterQueryAndNotInRolloutGroupsAndCompatibleAndUpdatable(@NotNull Pageable pageRequest, - @NotEmpty Collection groups, @NotNull String rsqlParam, + @NotEmpty Collection groups, @NotNull String targetFilterQuery, @NotNull DistributionSetType distributionSetType); + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + Slice findByNotInGEGroupAndNotInActiveActionGEWeightOrInRolloutAndTargetFilterQueryAndCompatibleAndUpdatable( + @NotNull Pageable pageRequest, final long rolloutId, final int weight, final long firstGroupId, @NotNull String targetFilterQuery, + @NotNull DistributionSetType distributionSetType); + + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + long countByActionsInRolloutGroup(final long rolloutGroupId); + /** * 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 diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutCreate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutCreate.java index b4b7487a3..023ccd32d 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutCreate.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutCreate.java @@ -103,6 +103,15 @@ public interface RolloutCreate { */ RolloutCreate weight(Integer weight); + /** + * Set if the rollout shall be dynamic + * + * @param dynamic + * for {@link Rollout#isDynamic()} + * @return updated builder instance + */ + RolloutCreate dynamic(boolean dynamic); + /** * set start * @@ -116,5 +125,4 @@ public interface RolloutCreate { * @return peek on current state of {@link Rollout} in the builder */ Rollout build(); - } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java index 5ec1ca168..96851a968 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java @@ -108,6 +108,11 @@ public interface Rollout extends NamedEntity { */ Optional getWeight(); + /** + * @return if the {@link Rollout} is dynamic. + */ + boolean isDynamic(); + /** * @return the stored access control context (if present) */ diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroup.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroup.java index 1c7ebbbfb..9eb819e58 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroup.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroup.java @@ -33,6 +33,11 @@ public interface RolloutGroup extends NamedEntity { */ RolloutGroup getParent(); + /** + * @return if the group is dynamic + */ + boolean isDynamic(); + /** * @return the {@link RolloutGroupSuccessCondition} for this group to * indicate when a group is successful 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 9680949f6..457c82a31 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 @@ -11,6 +11,7 @@ package org.eclipse.hawkbit.repository; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import javax.validation.ValidationException; @@ -229,9 +230,6 @@ public final class RolloutHelper { if (StringUtils.isEmpty(group.getTargetFilterQuery())) { return baseFilter; } - if (isRolloutRetried(baseFilter)) { - return baseFilter; - } return concatAndTargetFilters(baseFilter, group.getTargetFilterQuery()); } @@ -257,6 +255,23 @@ public final class RolloutHelper { } } + public static double toPercentFromTheRest(final RolloutGroup group, List rolloutGroups) { + final double percentFromRest; + // assume that the groups are served orderly + double toServePercent = 100; + for (final RolloutGroup next : rolloutGroups) { + if (next == group) { + break; + } + if (Objects.equals(next.getTargetFilterQuery(), group.getTargetFilterQuery())) { + toServePercent -= next.getTargetPercentage(); + } + } + percentFromRest = + toServePercent <= 1 ? 100 : Math.min(100, group.getTargetPercentage() * 100 / toServePercent); + return percentFromRest; + } + public static boolean isRolloutRetried(final String targetFilter) { return targetFilter.contains("failedrollout"); } 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 168527308..cb6a1a44f 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 @@ -9,8 +9,11 @@ */ package org.eclipse.hawkbit.repository.jpa; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -19,6 +22,7 @@ import javax.persistence.EntityManager; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.QuotaManagement; +import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.RolloutApprovalStrategy; import org.eclipse.hawkbit.repository.RolloutExecutor; import org.eclipse.hawkbit.repository.RolloutGroupManagement; @@ -28,9 +32,9 @@ import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.event.remote.RolloutGroupDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.RolloutStoppedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.RolloutUpdatedEvent; -import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; +import org.eclipse.hawkbit.repository.jpa.management.JpaRolloutManagement; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; @@ -64,7 +68,8 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionException; -import org.springframework.util.StringUtils; + +import static org.eclipse.hawkbit.repository.jpa.builder.JpaRolloutGroupCreate.addSuccessAndErrorConditionsAndActions; /** * A Jpa implementation of {@link RolloutExecutor} @@ -186,7 +191,8 @@ public class JpaRolloutExecutor implements RolloutExecutor { continue; } - final RolloutGroup filledGroup = fillRolloutGroupWithTargets(rollout, group); + final RolloutGroup filledGroup = fillRolloutGroupWithTargets(rollout, (JpaRolloutGroup) group, + rolloutGroups); if (RolloutGroupStatus.READY == filledGroup.getStatus()) { readyGroups++; totalTargets += filledGroup.getTotalTargets(); @@ -196,6 +202,11 @@ public class JpaRolloutExecutor implements RolloutExecutor { // When all groups are ready the rollout status can be changed to be // ready, too. if (readyGroups == rolloutGroups.size()) { + if (rollout.isDynamic()) { + // add first dynamic group one by using the last as a parent and as a pattern + createDynamicGroup(rollout, rolloutGroups.get(rolloutGroups.size() - 1), rolloutGroups.size(), RolloutGroupStatus.READY); + } + if (!rolloutApprovalStrategy.isApprovalNeeded(rollout)) { rollout.setStatus(RolloutStatus.READY); LOGGER.debug("rollout {} creation done. Switch to READY.", rollout.getId()); @@ -306,8 +317,18 @@ public class JpaRolloutExecutor implements RolloutExecutor { private void handleRunningRollout(final JpaRollout rollout) { LOGGER.debug("handleRunningRollout called for rollout {}", rollout.getId()); - final List rolloutGroupsRunning = rolloutGroupRepository.findByRolloutAndStatus(rollout, - RolloutGroupStatus.RUNNING); + if (rollout.isDynamic()) { + if (fillDynamicRolloutGroupsWithTargets(rollout)) { + LOGGER.debug("Dynamic group created for rollout {}", rollout.getId()); + return; + } + } + + final List rolloutGroupsRunning = + rollout.getRolloutGroups().stream() + .filter(group -> group.getStatus() == RolloutGroupStatus.RUNNING) + .map(JpaRolloutGroup.class::cast) + .toList(); if (rolloutGroupsRunning.isEmpty()) { // no running rollouts, probably there was an error @@ -317,7 +338,7 @@ public class JpaRolloutExecutor implements RolloutExecutor { executeLatestRolloutGroup(rollout); } else { LOGGER.debug("Rollout {} has {} running groups", rollout.getId(), rolloutGroupsRunning.size()); - executeRolloutGroups(rollout, rolloutGroupsRunning); + executeRolloutGroups(rollout, rolloutGroupsRunning, rollout.getRolloutGroups().get(rollout.getRolloutGroups().size() - 1)); } if (isRolloutComplete(rollout)) { @@ -371,35 +392,77 @@ public class JpaRolloutExecutor implements RolloutExecutor { return groupsActiveLeft == 0; } + private static final Comparator DESC_COMP = Comparator.comparingLong(RolloutGroup::getId).reversed(); private void executeLatestRolloutGroup(final JpaRollout rollout) { - final List latestRolloutGroup = rolloutGroupRepository - .findByRolloutAndStatusNotOrderByIdDesc(rollout, RolloutGroupStatus.SCHEDULED); + // was - rolloutGroupRepository.findByRolloutAndStatusNotOrderByIdDesc(rollout, RolloutGroupStatus.SCHEDULED); + final List 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)); } - private void executeRolloutGroups(final JpaRollout rollout, final List rolloutGroups) { - for (final JpaRolloutGroup rolloutGroup : rolloutGroups) { + private static final int DEFAULT_DYNAMIC_GROUP_EXPECTED = 100; + private static int expectedDynamicGroupSize(final List rolloutGroups) { + int expected = 0; + // gets the size of last static group (it is a pattern for dynamic groups) + for (final RolloutGroup rolloutGroup : rolloutGroups) { + if (rolloutGroup.isDynamic()) { + break; + } + expected = rolloutGroup.getTotalTargets(); + } + return expected <= 0 ? DEFAULT_DYNAMIC_GROUP_EXPECTED /* default if the last static group has been empty */ : expected; + } + // 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, final List rolloutGroups) { + if (group.isDynamic()) { + final int expected = expectedDynamicGroupSize(rolloutGroups); + return (RolloutGroup) Proxy.newProxyInstance( + RolloutGroup.class.getClassLoader(), + new Class[] {RolloutGroup.class}, + (proxy, method, args) -> { + if ("getTotalTargets".equals(method.getName())) { + return expected; + } else { + try { + return method.invoke(group, args); + } catch (final InvocationTargetException e) { + throw e.getCause() == null ? e : e.getCause(); + } + } + }); + } else { + return group; + } + } + + private void executeRolloutGroups(final JpaRollout rollout, final List rolloutGroups, final RolloutGroup lastRolloutGroup) { + for (final JpaRolloutGroup rolloutGroup : rolloutGroups) { final long targetCount = countTargetsFrom(rolloutGroup); if (rolloutGroup.getTotalTargets() != targetCount) { updateTotalTargetCount(rolloutGroup, targetCount); } + final RolloutGroup evalProxy = rolloutGroup == rolloutGroups.get(rolloutGroups.size() - 1) ? + evalProxy(rolloutGroup, rolloutGroups) : rolloutGroup; // error state check, do we need to stop the whole // rollout because of error? - final boolean isError = checkErrorState(rollout, rolloutGroup); + final boolean isError = checkErrorState(rollout, evalProxy); if (isError) { LOGGER.info("Rollout {} {} has error, calling error action", rollout.getName(), rollout.getId()); callErrorAction(rollout, rolloutGroup); } else { // not in error so check finished state, do we need to // start the next group? - final RolloutGroupSuccessCondition finishedCondition = rolloutGroup.getSuccessCondition(); - checkFinishCondition(rollout, rolloutGroup, finishedCondition); - if (isRolloutGroupComplete(rollout, rolloutGroup)) { + checkSuccessCondition(rollout, rolloutGroup, evalProxy, rolloutGroup.getSuccessCondition()); + if (!(rolloutGroup == lastRolloutGroup && rolloutGroup.isDynamic()) && isRolloutGroupComplete(rollout, rolloutGroup)) { rolloutGroup.setStatus(RolloutGroupStatus.FINISHED); rolloutGroupRepository.save(rolloutGroup); } @@ -418,7 +481,11 @@ public class JpaRolloutExecutor implements RolloutExecutor { } private long countTargetsFrom(final JpaRolloutGroup rolloutGroup) { - return rolloutGroupManagement.countTargetsOfRolloutsGroup(rolloutGroup.getId()); + if (rolloutGroup.isDynamic()) { + return targetManagement.countByActionsInRolloutGroup(rolloutGroup.getId()); + } else { + return rolloutGroupManagement.countTargetsOfRolloutsGroup(rolloutGroup.getId()); + } } private void callErrorAction(final Rollout rollout, final RolloutGroup rolloutGroup) { @@ -431,11 +498,11 @@ public class JpaRolloutExecutor implements RolloutExecutor { } private boolean isRolloutGroupComplete(final JpaRollout rollout, final JpaRolloutGroup rolloutGroup) { - final Long actionsLeftForRollout = ActionType.DOWNLOAD_ONLY == rollout.getActionType() - ? actionRepository.countByRolloutAndRolloutGroupAndStatusNotIn(rollout, rolloutGroup, - DOWNLOAD_ONLY_ACTION_TERMINATION_STATUSES) - : actionRepository.countByRolloutAndRolloutGroupAndStatusNotIn(rollout, rolloutGroup, - DEFAULT_ACTION_TERMINATION_STATUSES); + final Long actionsLeftForRollout = + actionRepository.countByRolloutAndRolloutGroupAndStatusNotIn( + rollout, rolloutGroup, + ActionType.DOWNLOAD_ONLY == rollout.getActionType() ? + DOWNLOAD_ONLY_ACTION_TERMINATION_STATUSES : DEFAULT_ACTION_TERMINATION_STATUSES); return actionsLeftForRollout == 0; } @@ -456,12 +523,12 @@ public class JpaRolloutExecutor implements RolloutExecutor { } } - private boolean checkFinishCondition(final Rollout rollout, final RolloutGroup rolloutGroup, - final RolloutGroupSuccessCondition finishCondition) { - LOGGER.trace("Checking finish condition {} on rolloutgroup {}", finishCondition, rolloutGroup); + private boolean checkSuccessCondition(final Rollout rollout, final RolloutGroup rolloutGroup, final RolloutGroup evalProxy, + final RolloutGroupSuccessCondition successCondition) { + LOGGER.trace("Checking finish condition {} on rolloutgroup {}", successCondition, rolloutGroup); try { - final boolean isFinished = evaluationManager.getSuccessConditionEvaluator(finishCondition).eval(rollout, - rolloutGroup, rolloutGroup.getSuccessConditionExp()); + final boolean isFinished = evaluationManager.getSuccessConditionEvaluator(successCondition).eval(rollout, + evalProxy, rolloutGroup.getSuccessConditionExp()); if (isFinished) { LOGGER.debug("Rolloutgroup {} is finished, starting next group", rolloutGroup); executeRolloutGroupSuccessAction(rollout, rolloutGroup); @@ -471,7 +538,7 @@ public class JpaRolloutExecutor implements RolloutExecutor { return isFinished; } catch (final EvaluatorNotConfiguredException e) { LOGGER.error("Something bad happened when accessing the finish condition or success action bean {}", - finishCondition.name(), e); + successCondition.name(), e); return false; } } @@ -513,18 +580,12 @@ public class JpaRolloutExecutor implements RolloutExecutor { return scheduledGroups == groupsToBeScheduled.size(); } - private RolloutGroup fillRolloutGroupWithTargets(final JpaRollout rollout, final RolloutGroup group1) { + private RolloutGroup fillRolloutGroupWithTargets(final JpaRollout rollout, final JpaRolloutGroup group, + final List rolloutGroups) { RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING); - final JpaRolloutGroup group = (JpaRolloutGroup) group1; - - final String baseFilter = RolloutHelper.getTargetFilterQuery(rollout); - final String groupTargetFilter; - if (StringUtils.hasText(group.getTargetFilterQuery())) { - groupTargetFilter = baseFilter + ";" + group.getTargetFilterQuery(); - } else { - groupTargetFilter = baseFilter; - } + final String groupTargetFilter = RolloutHelper.getGroupTargetFilter( + RolloutHelper.getTargetFilterQuery(rollout), group); final List readyGroups = RolloutHelper.getGroupsByStatusIncludingGroup(rollout.getRolloutGroups(), RolloutGroupStatus.READY, group); @@ -541,28 +602,38 @@ public class JpaRolloutExecutor implements RolloutExecutor { count -> targetManagement.countByFailedRolloutAndNotInRolloutGroups(readyGroups, RolloutHelper.getIdFromRetriedTargetFilter(rollout.getTargetFilterQuery()))); } - final long expectedInGroup = Math - .round((double) (group.getTargetPercentage() / 100) * (double) targetsInGroupFilter); + + final double percentFromTheRest; + if (rollout.isNewStyleTargetPercent()) { // new style percent - total percent + percentFromTheRest = RolloutHelper.toPercentFromTheRest(group, rolloutGroups); + } else { // old style percent - percent from rest + percentFromTheRest = group.getTargetPercentage(); + } + + final long expectedInGroup = Math.round(percentFromTheRest * targetsInGroupFilter / 100); final long currentlyInGroup = DeploymentHelper.runInNewTransaction(txManager, "countRolloutTargetGroupByRolloutGroup", count -> rolloutTargetGroupRepository.countByRolloutGroup(group)); - // Switch the Group status to READY, when there are enough Targets in - // the Group + // Switch the Group status to READY, when there are enough Targets in the Group if (currentlyInGroup >= expectedInGroup) { group.setStatus(RolloutGroupStatus.READY); return rolloutGroupRepository.save(group); } try { - long targetsLeftToAdd = expectedInGroup - currentlyInGroup; do { // Add up to TRANSACTION_TARGETS of the left targets // In case a TransactionException is thrown this loop aborts - targetsLeftToAdd -= assignTargetsToGroupInNewTransaction(rollout, group, groupTargetFilter, + final long assigned = assignTargetsToGroupInNewTransaction(rollout, group, groupTargetFilter, Math.min(TRANSACTION_TARGETS, targetsLeftToAdd)); + if (assigned == 0) { + break; // percent > 100 or some could have disappeared + } else { + targetsLeftToAdd -= assigned; + } } while (targetsLeftToAdd > 0); group.setStatus(RolloutGroupStatus.READY); @@ -579,7 +650,6 @@ public class JpaRolloutExecutor implements RolloutExecutor { private Long assignTargetsToGroupInNewTransaction(final JpaRollout rollout, final RolloutGroup group, final String targetFilter, final long limit) { - return DeploymentHelper.runInNewTransaction(txManager, "assignTargetsToRolloutGroup", status -> { final PageRequest pageRequest = PageRequest.of(0, Math.toIntExact(limit)); final List readyGroups = RolloutHelper.getGroupsByStatusIncludingGroup(rollout.getRolloutGroups(), @@ -587,10 +657,10 @@ public class JpaRolloutExecutor implements RolloutExecutor { Slice targets; if (!RolloutHelper.isRolloutRetried(rollout.getTargetFilterQuery())) { targets = targetManagement.findByTargetFilterQueryAndNotInRolloutGroupsAndCompatibleAndUpdatable( - pageRequest, readyGroups, targetFilter, rollout.getDistributionSet().getType()); + pageRequest, readyGroups, targetFilter, rollout.getDistributionSet().getType()); } else { targets = targetManagement.findByFailedRolloutAndNotInRolloutGroups( - pageRequest, readyGroups, RolloutHelper.getIdFromRetriedTargetFilter(rollout.getTargetFilterQuery())); + pageRequest, readyGroups, RolloutHelper.getIdFromRetriedTargetFilter(rollout.getTargetFilterQuery())); } createAssignmentOfTargetsToGroup(targets, group); @@ -599,6 +669,121 @@ public class JpaRolloutExecutor implements RolloutExecutor { }); } + // return if group change is made + private boolean fillDynamicRolloutGroupsWithTargets(final JpaRollout rollout) { + RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.RUNNING); + final List rolloutGroups = rollout.getRolloutGroups(); + + final JpaRolloutGroup group = (JpaRolloutGroup)rolloutGroups.get(rolloutGroups.size() - 1); + + if (group.getStatus() == RolloutGroupStatus.FINISHED) { + createDynamicGroup(rollout, group, rolloutGroups.size(), RolloutGroupStatus.RUNNING); + return true; + } else if (group.getStatus() != RolloutGroupStatus.RUNNING) { + return false; + } + + // expected as last full group - last static or previously filled in dynamic group + final long expectedInGroup = expectedDynamicGroupSize(rolloutGroups); + + final long currentlyInGroup = group.getTotalTargets(); + if (currentlyInGroup >= expectedInGroup) { + // the last one is filled. create new and start filling it + createDynamicGroup(rollout, group, rolloutGroups.size(), RolloutGroupStatus.SCHEDULED); + return true; + } + + // there are more to be filled for that group + // do this until there are more matching + try { + long targetsLeftToAdd = expectedInGroup - currentlyInGroup; + final String groupTargetFilter = RolloutHelper.getGroupTargetFilter( + // don't use RolloutHelper.getTargetFilterQuery(rollout) + // since it contains condition for device to be created + // before the rollout + rollout.getTargetFilterQuery(), group); + long newActions = 0; + do { + // Add up to TRANSACTION_TARGETS actions of the left targets + // In case a TransactionException is thrown this loop aborts + final long createdActions = createActionsForDynamicGroupInNewTransaction(rollout, group, groupTargetFilter, + Math.min(TRANSACTION_TARGETS, targetsLeftToAdd)); + if (createdActions == 0) { + break; // no more to assign + } else { + newActions += createdActions; + targetsLeftToAdd -= createdActions; + } + } while (targetsLeftToAdd > 0); + + if (newActions > 0) { + updateTotalTargetCount(group, group.getTotalTargets() + newActions); + + if (targetsLeftToAdd == 0) { + // this is filled create a new one in sheduled state + createDynamicGroup(rollout, group, rolloutGroups.size(), RolloutGroupStatus.SCHEDULED); + return true; + } + + // TODO - try to return false and proceed with handleRunningRollout + // the problem is that OptimisticLockException is thrown in that case + return true; + } + } catch (final TransactionException e) { + LOGGER.warn("Transaction assigning Targets to RolloutGroup failed", e); + } + return false; + } + + private void createDynamicGroup(final JpaRollout rollout, final RolloutGroup lastGroup, final int groupCount, final RolloutGroupStatus status) { + final JpaRolloutGroup group = new JpaRolloutGroup(); + final String nameAndDesc = "group-" + (groupCount + 1) + "-dynamic"; + group.setDynamic(true); + group.setName(nameAndDesc); + group.setDescription(nameAndDesc); + group.setRollout(rollout); + group.setParent(lastGroup); + // no need to be filled with targets, directly in ready (if first on create - it will be scheduled on start) + // or scheduled state (for next dynamic groups) + group.setStatus(status); + group.setConfirmationRequired(lastGroup.isConfirmationRequired()); + + group.setTargetPercentage(lastGroup.getTargetPercentage()); + group.setTargetFilterQuery(lastGroup.getTargetFilterQuery()); + + addSuccessAndErrorConditionsAndActions(group, lastGroup.getSuccessCondition(), + lastGroup.getSuccessConditionExp(), lastGroup.getSuccessAction(), + lastGroup.getSuccessActionExp(), lastGroup.getErrorCondition(), + lastGroup.getErrorConditionExp(), lastGroup.getErrorAction(), + lastGroup.getErrorActionExp()); + + final JpaRolloutGroup savedGroup = rolloutGroupRepository.save(group); + rollout.setRolloutGroupsCreated(rollout.getRolloutGroupsCreated() + 1); + rolloutRepository.save(rollout); + ((JpaRolloutManagement) rolloutManagement).publishRolloutGroupCreatedEventAfterCommit(savedGroup, rollout); + } + + private Long createActionsForDynamicGroupInNewTransaction(final JpaRollout rollout, final RolloutGroup group, + final String targetFilter, final long limit) { + return DeploymentHelper.runInNewTransaction(txManager, "createActionsForRolloutDynamicGroup", status -> { + final PageRequest pageRequest = PageRequest.of(0, Math.toIntExact(limit)); + final Slice targets = targetManagement.findByNotInGEGroupAndNotInActiveActionGEWeightOrInRolloutAndTargetFilterQueryAndCompatibleAndUpdatable( + pageRequest, + rollout.getId(), rollout.getWeight().orElse(1000), // Dynamic rollouts shall always have weight! + rolloutGroupRepository.findByRolloutOrderByIdAsc(rollout).get(0).getId(), + targetFilter, rollout.getDistributionSet().getType()); + + if (targets.getNumberOfElements() > 0) { + final DistributionSet distributionSet = rollout.getDistributionSet(); + final ActionType actionType = rollout.getActionType(); + final long forceTime = rollout.getForcedTime(); + createActions(targets.getContent(), distributionSet, actionType, forceTime, rollout, group); + } + + return Long.valueOf(targets.getNumberOfElements()); + }); + } + /** * Schedules a group of the rollout. Scheduled Actions are created to * achieve this. The creation of those Actions is allowed to fail. @@ -613,8 +798,10 @@ public class JpaRolloutExecutor implements RolloutExecutor { } if (actionsLeft <= 0) { - group.setStatus(RolloutGroupStatus.SCHEDULED); - rolloutGroupRepository.save(group); + if (group.getStatus() != RolloutGroupStatus.SCHEDULED && group.getStatus() != RolloutGroupStatus.RUNNING) { // dynamic groups could already be running + group.setStatus(RolloutGroupStatus.SCHEDULED); + rolloutGroupRepository.save(group); + } return true; } return false; @@ -625,8 +812,7 @@ public class JpaRolloutExecutor implements RolloutExecutor { try { long actionsCreated; do { - actionsCreated = createActionsForTargetsInNewTransaction(rollout.getId(), group.getId(), - TRANSACTION_TARGETS); + actionsCreated = createActionsForTargetsInNewTransaction(rollout, group, TRANSACTION_TARGETS); totalActionsCreated += actionsCreated; } while (actionsCreated > 0); @@ -637,21 +823,17 @@ public class JpaRolloutExecutor implements RolloutExecutor { return totalActionsCreated; } - private Long createActionsForTargetsInNewTransaction(final long rolloutId, final long groupId, final int limit) { + private Long createActionsForTargetsInNewTransaction( + final Rollout rollout, final RolloutGroup group, final int limit) { return DeploymentHelper.runInNewTransaction(txManager, "createActionsForTargets", status -> { - final PageRequest pageRequest = PageRequest.of(0, limit); - final Rollout rollout = rolloutRepository.findById(rolloutId) - .orElseThrow(() -> new EntityNotFoundException(Rollout.class, rolloutId)); - final RolloutGroup group = rolloutGroupRepository.findById(groupId) - .orElseThrow(() -> new EntityNotFoundException(RolloutGroup.class, groupId)); + final Slice targets = + targetManagement.findByInRolloutGroupWithoutAction(PageRequest.of(0, limit), group.getId()); - final DistributionSet distributionSet = rollout.getDistributionSet(); - final ActionType actionType = rollout.getActionType(); - final long forceTime = rollout.getForcedTime(); - - final Slice targets = targetManagement.findByInRolloutGroupWithoutAction(pageRequest, groupId); if (targets.getNumberOfElements() > 0) { - createScheduledAction(targets.getContent(), distributionSet, actionType, forceTime, rollout, group); + final DistributionSet distributionSet = rollout.getDistributionSet(); + final ActionType actionType = rollout.getActionType(); + final long forceTime = rollout.getForcedTime(); + createActions(targets.getContent(), distributionSet, actionType, forceTime, rollout, group); } return Long.valueOf(targets.getNumberOfElements()); @@ -665,9 +847,9 @@ public class JpaRolloutExecutor implements RolloutExecutor { /** * Creates an action entry into the action repository. In case of existing * scheduled actions the scheduled actions gets canceled. A scheduled action - * is created in-active. + * is created in-active for static and running for dynamic groups. */ - private void createScheduledAction(final Collection targets, final DistributionSet distributionSet, + private void createActions(final Collection targets, final DistributionSet distributionSet, final ActionType actionType, final Long forcedTime, final Rollout rollout, final RolloutGroup rolloutGroup) { // cancel all current scheduled actions for this target. E.g. an action @@ -677,16 +859,15 @@ public class JpaRolloutExecutor implements RolloutExecutor { final List targetIds = targets.stream().map(Target::getId).collect(Collectors.toList()); deploymentManagement.cancelInactiveScheduledActionsForTargets(targetIds); targets.forEach(target -> { - assertActionsPerTargetQuota(target, 1); final JpaAction action = new JpaAction(); action.setTarget(target); - action.setActive(false); + action.setActive(rolloutGroup.isDynamic()); action.setDistributionSet(distributionSet); action.setActionType(actionType); action.setForcedTime(forcedTime); - action.setStatus(Status.SCHEDULED); + action.setStatus(rolloutGroup.isDynamic() ? Status.RUNNING : Status.SCHEDULED); action.setRollout(rollout); action.setRolloutGroup(rolloutGroup); action.setInitiatedBy(rollout.getCreatedBy()); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index cd5ce9836..6bc1dedb9 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -655,10 +655,11 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final VirtualPropertyReplacer virtualPropertyReplacer, final DistributionSetManagement distributionSetManagement, final QuotaManagement quotaManagement, final JpaProperties properties, final TenantConfigurationManagement tenantConfigurationManagement, + final RepositoryProperties repositoryProperties, final SystemSecurityContext systemSecurityContext, final ContextAware contextAware) { return new JpaTargetFilterQueryManagement(targetFilterQueryRepository, targetManagement, virtualPropertyReplacer, distributionSetManagement, quotaManagement, properties.getDatabase(), - tenantConfigurationManagement, systemSecurityContext, contextAware); + tenantConfigurationManagement, repositoryProperties, systemSecurityContext, contextAware); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutCreate.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutCreate.java index cfc936add..5ae8af95d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutCreate.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutCreate.java @@ -16,11 +16,17 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; public class JpaRolloutCreate extends AbstractRolloutUpdateCreate implements RolloutCreate { private final DistributionSetManagement distributionSetManagement; + private boolean dynamic; JpaRolloutCreate(final DistributionSetManagement distributionSetManagement) { this.distributionSetManagement = distributionSetManagement; } + public RolloutCreate dynamic(final boolean dynamic) { + this.dynamic = dynamic; + return this; + } + @Override public JpaRollout build() { final JpaRollout rollout = new JpaRollout(); @@ -31,6 +37,7 @@ public class JpaRolloutCreate extends AbstractRolloutUpdateCreate rollout.setTargetFilterQuery(targetFilterQuery); rollout.setStartAt(startAt); rollout.setWeight(weight); + rollout.setDynamic(dynamic); if (actionType != null) { rollout.setActionType(actionType); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/AbstractDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/AbstractDsAssignmentStrategy.java index 7370810d0..70b5b563f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/AbstractDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/AbstractDsAssignmentStrategy.java @@ -19,6 +19,7 @@ import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; +import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.event.remote.CancelTargetAssignmentEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; @@ -67,12 +68,13 @@ public abstract class AbstractDsAssignmentStrategy { private final QuotaManagement quotaManagement; private final BooleanSupplier multiAssignmentsConfig; private final BooleanSupplier confirmationFlowConfig; + private final RepositoryProperties repositoryProperties; AbstractDsAssignmentStrategy(final TargetRepository targetRepository, final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder, final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig, - final BooleanSupplier confirmationFlowConfig) { + final BooleanSupplier confirmationFlowConfig, final RepositoryProperties repositoryProperties) { this.targetRepository = targetRepository; this.afterCommit = afterCommit; this.eventPublisherHolder = eventPublisherHolder; @@ -81,6 +83,7 @@ public abstract class AbstractDsAssignmentStrategy { this.quotaManagement = quotaManagement; this.multiAssignmentsConfig = multiAssignmentsConfig; this.confirmationFlowConfig = confirmationFlowConfig; + this.repositoryProperties = repositoryProperties; } /** @@ -252,7 +255,9 @@ public abstract class AbstractDsAssignmentStrategy { final JpaAction actionForTarget = new JpaAction(); actionForTarget.setActionType(targetWithActionType.getActionType()); actionForTarget.setForcedTime(targetWithActionType.getForceTime()); - actionForTarget.setWeight(targetWithActionType.getWeight()); + actionForTarget.setWeight( + targetWithActionType.getWeight() == null ? + repositoryProperties.getActionWeightIfAbsent() : targetWithActionType.getWeight()); actionForTarget.setActive(true); actionForTarget.setTarget(target); actionForTarget.setDistributionSet(set); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDeploymentManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDeploymentManagement.java index eca4eb209..7e5c17554 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDeploymentManagement.java @@ -176,10 +176,10 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl this.txManager = txManager; onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository, quotaManagement, this::isMultiAssignmentsEnabled, - this::isConfirmationFlowEnabled); + this::isConfirmationFlowEnabled, repositoryProperties); offlineDsAssignmentStrategy = new OfflineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository, quotaManagement, - this::isMultiAssignmentsEnabled, this::isConfirmationFlowEnabled); + this::isMultiAssignmentsEnabled, this::isConfirmationFlowEnabled, repositoryProperties); this.tenantConfigurationManagement = tenantConfigurationManagement; this.systemSecurityContext = systemSecurityContext; this.tenantAware = tenantAware; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java index 84663aa80..c1a6ef97a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java @@ -17,6 +17,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -26,6 +27,7 @@ import javax.validation.ValidationException; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.QuotaManagement; +import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.RolloutApprovalStrategy; import org.eclipse.hawkbit.repository.RolloutFields; import org.eclipse.hawkbit.repository.RolloutHelper; @@ -109,6 +111,9 @@ public class JpaRolloutManagement implements RolloutManagement { RolloutStatus.CREATING, RolloutStatus.PAUSED, RolloutStatus.READY, RolloutStatus.STARTING, RolloutStatus.WAITING_FOR_APPROVAL, RolloutStatus.APPROVAL_DENIED); + @Autowired + private RepositoryProperties repositoryProperties; + @Autowired private RolloutRepository rolloutRepository; @@ -219,6 +224,9 @@ public class JpaRolloutManagement implements RolloutManagement { throw new ValidationException(errMsg); } rollout.setTotalTargets(totalTargets); + if (rollout.getWeight().isEmpty()) { + rollout.setWeight(repositoryProperties.getActionWeightIfAbsent()); + } contextAware.getCurrentContext().ifPresent(rollout::setAccessControlContext); return rolloutRepository.save(rollout); } @@ -247,7 +255,15 @@ public class JpaRolloutManagement implements RolloutManagement { addSuccessAndErrorConditionsAndActions(group, conditions); - group.setTargetPercentage(1.0F / (amountOfGroups - i) * 100); + // total percent of the all devices. Before, it was relative percent - + // the percent of the "rest" of the devices. Thus, if you have + // first a group 10% (the rest is 90%) and the second group is 50% + // then the percent would be 50% of 90% - 45%. + // This is very unintuitive and is switched in order to be interpreted easier. + // the "new style" (vs "old style") rollouts could be detected by + // JpaRollout#isNewStyleTargetPercent (which uses that old style rollouts + // have null as dynamic + group.setTargetPercentage(100.0F / amountOfGroups); lastSavedGroup = rolloutGroupRepository.save(group); publishRolloutGroupCreatedEventAfterCommit(lastSavedGroup, rollout); @@ -350,7 +366,7 @@ public class JpaRolloutManagement implements RolloutManagement { return group; } - private void publishRolloutGroupCreatedEventAfterCommit(final RolloutGroup group, final Rollout rollout) { + public void publishRolloutGroupCreatedEventAfterCommit(final RolloutGroup group, final Rollout rollout) { afterCommit.afterCommit(() -> eventPublisherHolder.getEventPublisher().publishEvent( new RolloutGroupCreatedEvent(group, rollout.getId(), eventPublisherHolder.getApplicationId()))); } @@ -672,11 +688,13 @@ public class JpaRolloutManagement implements RolloutManagement { realTargetsInGroup = targetsInGroupFilter - overlappingTargets; } + // new style percent - total percent + final double percentFromRest = RolloutHelper.toPercentFromTheRest(group, groups); + final long reducedTargetsInGroup = Math - .round(group.getTargetPercentage() / 100 * (double) realTargetsInGroup); + .round(percentFromRest / 100 * (double) realTargetsInGroup); groupTargetCounts.add(reducedTargetsInGroup); unusedTargetsCount += realTargetsInGroup - reducedTargetsInGroup; - } return new RolloutGroupsValidation(totalTargets, groupTargetCounts); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetFilterQueryManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetFilterQueryManagement.java index e99395e32..f4ce0d726 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetFilterQueryManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetFilterQueryManagement.java @@ -17,6 +17,7 @@ import javax.validation.constraints.NotNull; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.QuotaManagement; +import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.TargetFields; import org.eclipse.hawkbit.repository.TargetFilterQueryFields; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; @@ -84,6 +85,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme private final DistributionSetManagement distributionSetManagement; private final QuotaManagement quotaManagement; private final TenantConfigurationManagement tenantConfigurationManagement; + private final RepositoryProperties repositoryProperties; private final SystemSecurityContext systemSecurityContext; private final ContextAware contextAware; @@ -93,6 +95,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme final TargetManagement targetManagement, final VirtualPropertyReplacer virtualPropertyReplacer, final DistributionSetManagement distributionSetManagement, final QuotaManagement quotaManagement, final Database database, final TenantConfigurationManagement tenantConfigurationManagement, + final RepositoryProperties repositoryProperties, final SystemSecurityContext systemSecurityContext, final ContextAware contextAware) { this.targetFilterQueryRepository = targetFilterQueryRepository; this.targetManagement = targetManagement; @@ -101,6 +104,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme this.quotaManagement = quotaManagement; this.database = database; this.tenantConfigurationManagement = tenantConfigurationManagement; + this.repositoryProperties = repositoryProperties; this.systemSecurityContext = systemSecurityContext; this.contextAware = contextAware; } @@ -270,7 +274,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme targetFilterQuery.setAccessControlContext(null); targetFilterQuery.setAutoAssignDistributionSet(null); targetFilterQuery.setAutoAssignActionType(null); - targetFilterQuery.setAutoAssignWeight(null); + targetFilterQuery.setAutoAssignWeight(0); targetFilterQuery.setAutoAssignInitiatedBy(null); targetFilterQuery.setConfirmationRequired(false); } else { @@ -284,9 +288,11 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme contextAware.getCurrentContext().ifPresent(targetFilterQuery::setAccessControlContext); targetFilterQuery.setAutoAssignInitiatedBy(contextAware.getCurrentUsername()); targetFilterQuery.setAutoAssignActionType(sanitizeAutoAssignActionType(update.getActionType())); - targetFilterQuery.setAutoAssignWeight(update.getWeight()); - final boolean confirmationRequired = update.isConfirmationRequired() == null ? isConfirmationFlowEnabled() - : update.isConfirmationRequired(); + targetFilterQuery.setAutoAssignWeight( + update.getWeight() == null ? repositoryProperties.getActionWeightIfAbsent() : update.getWeight()); + final boolean confirmationRequired = + update.isConfirmationRequired() == null ? + isConfirmationFlowEnabled() : update.isConfirmationRequired(); targetFilterQuery.setConfirmationRequired(confirmationRequired); } return targetFilterQueryRepository.save(targetFilterQuery); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java index 4ff027df1..225228e7f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java @@ -698,6 +698,27 @@ public class JpaTargetManagement implements TargetManagement { .map(Target.class::cast); } + @Override + public Slice findByNotInGEGroupAndNotInActiveActionGEWeightOrInRolloutAndTargetFilterQueryAndCompatibleAndUpdatable( + final Pageable pageRequest, final long rolloutId, final int weight, final long firstGroupId, final String targetFilterQuery, + final DistributionSetType distributionSetType) { + return targetRepository + .findAllWithoutCount(AccessController.Operation.UPDATE, + combineWithAnd(List.of( + RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, + virtualPropertyReplacer, database), + TargetSpecifications.isNotInGERolloutGroup(firstGroupId), + TargetSpecifications.hasNoActiveActionWithGEWeightOrInRollout(weight, rolloutId), + TargetSpecifications.isCompatibleWithDistributionSetType(distributionSetType.getId()))), + pageRequest) + .map(Target.class::cast); + } + + @Override + public long countByActionsInRolloutGroup(final long rolloutGroupId) { + return targetRepository.count(TargetSpecifications.isInActionRolloutGroup(rolloutGroupId)); + } + @Override public Slice findByFailedRolloutAndNotInRolloutGroups(Pageable pageRequest, Collection groups, String rolloutId) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OfflineDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OfflineDsAssignmentStrategy.java index 6df9497f0..f7fc19e63 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OfflineDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OfflineDsAssignmentStrategy.java @@ -19,6 +19,7 @@ import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; +import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; import org.eclipse.hawkbit.repository.jpa.acm.AccessController; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; @@ -52,9 +53,9 @@ public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder, final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig, - final BooleanSupplier confirmationFlowConfig) { + final BooleanSupplier confirmationFlowConfig, final RepositoryProperties repositoryProperties) { super(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository, - quotaManagement, multiAssignmentsConfig, confirmationFlowConfig); + quotaManagement, multiAssignmentsConfig, confirmationFlowConfig, repositoryProperties); } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OnlineDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OnlineDsAssignmentStrategy.java index aef827234..56cb4a561 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OnlineDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OnlineDsAssignmentStrategy.java @@ -19,6 +19,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.hawkbit.repository.QuotaManagement; +import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.event.remote.MultiActionAssignEvent; import org.eclipse.hawkbit.repository.event.remote.MultiActionCancelEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; @@ -55,9 +56,9 @@ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder, final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig, - final BooleanSupplier confirmationFlowConfig) { + final BooleanSupplier confirmationFlowConfig, final RepositoryProperties repositoryProperties) { super(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository, - quotaManagement, multiAssignmentsConfig, confirmationFlowConfig); + quotaManagement, multiAssignmentsConfig, confirmationFlowConfig, repositoryProperties); } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java index 78f820c33..b97a29600 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java @@ -140,6 +140,9 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event @Max(Action.WEIGHT_MAX) private Integer weight; + @Column(name = "is_dynamic") // dynamic is reserved keyword in some databases + private Boolean dynamic; + @Column(name = "access_control_context", nullable = true) private String accessControlContext; @@ -225,6 +228,21 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event this.weight = weight; } + @Override + public boolean isDynamic() { + return Boolean.TRUE.equals(dynamic); + } + + public void setDynamic(final Boolean dynamic) { + this.dynamic = dynamic; + } + + // dynamic is null only for old rollouts - could be used for distinguishing + // old once from the other + public boolean isNewStyleTargetPercent() { + return dynamic != null; + } + public Optional getAccessControlContext() { return Optional.ofNullable(accessControlContext); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRolloutGroup.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRolloutGroup.java index 68338f60e..ad6011e10 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRolloutGroup.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRolloutGroup.java @@ -78,6 +78,9 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr @JoinColumn(name = "parent_id") private JpaRolloutGroup parent; + @Column(name = "is_dynamic") // dynamic is reserved keyword in some databases + private boolean dynamic; + @Column(name = "success_condition", nullable = false) @NotNull private RolloutGroupSuccessCondition successCondition = RolloutGroupSuccessCondition.THRESHOLD; @@ -156,6 +159,15 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr return parent; } + @Override + public boolean isDynamic() { + return dynamic; + } + + public void setDynamic(final boolean dynamic) { + this.dynamic = dynamic; + } + public void setParent(final RolloutGroup parent) { this.parent = (JpaRolloutGroup) parent; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetFilterQuery.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetFilterQuery.java index 01bb072dc..81d95ca23 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetFilterQuery.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetFilterQuery.java @@ -113,7 +113,7 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity this.query = query; this.autoAssignDistributionSet = (JpaDistributionSet) autoAssignDistributionSet; this.autoAssignActionType = autoAssignActionType; - this.autoAssignWeight = autoAssignWeight; + this.autoAssignWeight = autoAssignWeight == null ? 0 : autoAssignWeight; this.confirmationRequired = confirmationRequired; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/StartNextGroupRolloutGroupSuccessAction.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/StartNextGroupRolloutGroupSuccessAction.java index f429c6f43..d79c389ee 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/StartNextGroupRolloutGroupSuccessAction.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/StartNextGroupRolloutGroupSuccessAction.java @@ -75,11 +75,15 @@ public class StartNextGroupRolloutGroupSuccessAction implements RolloutGroupActi final List findByRolloutGroupParent = rolloutGroupRepository .findByParentIdAndStatus(rolloutGroup.getId(), RolloutGroupStatus.SCHEDULED); findByRolloutGroupParent.forEach(nextGroup -> { - logger.debug("Rolloutgroup {} is finished, starting next group", nextGroup); - nextGroup.setStatus(RolloutGroupStatus.FINISHED); - rolloutGroupRepository.save(nextGroup); - // find the next group to set in running state - startNextGroup(rollout, nextGroup); + if (nextGroup.isDynamic()) { + nextGroup.setStatus(RolloutGroupStatus.RUNNING); + } else { + logger.debug("Rolloutgroup {} is finished, starting next group", nextGroup); + nextGroup.setStatus(RolloutGroupStatus.FINISHED); + rolloutGroupRepository.save(nextGroup); + // find the next group to set in running state + startNextGroup(rollout, nextGroup); + } }); } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/ThresholdRolloutGroupErrorCondition.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/ThresholdRolloutGroupErrorCondition.java index ca53fa333..e10a6f711 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/ThresholdRolloutGroupErrorCondition.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/ThresholdRolloutGroupErrorCondition.java @@ -39,8 +39,7 @@ public class ThresholdRolloutGroupErrorCondition @Override public boolean eval(final Rollout rollout, final RolloutGroup rolloutGroup, final String expression) { - final Long totalGroup = actionRepository.countByRolloutAndRolloutGroup((JpaRollout) rollout, - (JpaRolloutGroup) rolloutGroup); + final long totalGroup = rolloutGroup.getTotalTargets(); final Long error = actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(rollout.getId(), rolloutGroup.getId(), Action.Status.ERROR); try { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/ThresholdRolloutGroupSuccessCondition.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/ThresholdRolloutGroupSuccessCondition.java index a5ef90c3f..712280862 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/ThresholdRolloutGroupSuccessCondition.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/ThresholdRolloutGroupSuccessCondition.java @@ -37,7 +37,6 @@ public class ThresholdRolloutGroupSuccessCondition @Override public boolean eval(final Rollout rollout, final RolloutGroup rolloutGroup, final String expression) { - final long totalGroup = rolloutGroup.getTotalTargets(); if (totalGroup == 0) { // in case e.g. targets has been deleted we don't have any 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 13d1cf755..c231d3db8 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 @@ -33,7 +33,9 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType_; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet_; +import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup_; +import org.eclipse.hawkbit.repository.jpa.model.JpaRollout_; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag_; @@ -459,9 +461,24 @@ public final class TargetSpecifications { return (targetRoot, query, cb) -> { final ListJoin rolloutTargetJoin = targetRoot .join(JpaTarget_.rolloutTargetGroup, JoinType.LEFT); - final Predicate inRolloutGroups = rolloutTargetJoin.get(RolloutTargetGroup_.rolloutGroup) - .get(JpaRolloutGroup_.id).in(groups); - rolloutTargetJoin.on(inRolloutGroups); + rolloutTargetJoin.on(rolloutTargetJoin.get(RolloutTargetGroup_.rolloutGroup) + .get(JpaRolloutGroup_.id).in(groups)); + return cb.isNull(rolloutTargetJoin.get(RolloutTargetGroup_.target)); + }; + } + + /** + * {@link Specification} for retrieving {@link Target}s that are not in + * any {@link RolloutGroup}s + * + * @return the {@link Target} {@link Specification} + */ + public static Specification isNotInGERolloutGroup(final long groupId) { + return (targetRoot, query, cb) -> { + final ListJoin rolloutTargetJoin = targetRoot + .join(JpaTarget_.rolloutTargetGroup, JoinType.LEFT); + rolloutTargetJoin.on(cb.ge(rolloutTargetJoin.get(RolloutTargetGroup_.rolloutGroup) + .get(JpaRolloutGroup_.id), groupId)); return cb.isNull(rolloutTargetJoin.get(RolloutTargetGroup_.target)); }; } @@ -605,4 +622,36 @@ public final class TargetSpecifications { }; } + /** + * {@link Specification} for retrieving {@link Target}s that have no active (non-finished) action + * with great or equal weight (GEWeight. + * + * @param weight the referent weight + * @return the {@link Target} {@link Specification} + */ + public static Specification hasNoActiveActionWithGEWeightOrInRollout(final int weight, final long rolloutId) { + return (targetRoot, query, cb) -> { + final ListJoin actionsJoin = targetRoot.join(JpaTarget_.actions, JoinType.LEFT); + actionsJoin.on( + cb.or( + cb.gt(actionsJoin.get(JpaAction_.weight), weight), + cb.and( + cb.equal(actionsJoin.get(JpaAction_.weight), weight), + cb.ge(actionsJoin.get(JpaAction_.ROLLOUT).get(JpaRollout_.ID), rolloutId)))); + // another, but probably heavier variant +// actionsJoin.on( +// cb.or( +// // in rollout +// cb.equal(actionsJoin.get(JpaAction_.ROLLOUT).get(JpaRollout_.ID), rolloutId), +// // or, in newer rollout with greater or equal weight +// cb.and( +// cb.gt(actionsJoin.get(JpaAction_.ROLLOUT).get(JpaRollout_.ID), rolloutId), +// cb.ge(actionsJoin.get(JpaAction_.weight), weight)), +// // or, in older with greater status +// cb.and( +// cb.lt(actionsJoin.get(JpaAction_.ROLLOUT).get(JpaRollout_.ID), rolloutId), +// cb.gt(actionsJoin.get(JpaAction_.weight), weight)))); + return cb.isNull(actionsJoin.get(JpaAction_.id)); + }; + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/WeightValidationHelper.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/WeightValidationHelper.java index 73aef894e..4a463ff0b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/WeightValidationHelper.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/WeightValidationHelper.java @@ -123,9 +123,7 @@ public final class WeightValidationHelper { final boolean bypassWeightEnforcement = true; final boolean multiAssignmentsEnabled = TenantConfigHelper .usingContext(systemSecurityContext, tenantConfigurationManagement).isMultiAssignmentsEnabled(); - if (!multiAssignmentsEnabled && hasWeight) { - throw new MultiAssignmentIsNotEnabledException(); - } else if (bypassWeightEnforcement) { + if (bypassWeightEnforcement) { return; } else if (multiAssignmentsEnabled && hasNoWeight) { throw new NoWeightProvidedInMultiAssignmentModeException(); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_28__add_dynamic_rollout___DB2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_28__add_dynamic_rollout___DB2.sql new file mode 100644 index 000000000..cff85ae84 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_28__add_dynamic_rollout___DB2.sql @@ -0,0 +1,9 @@ +ALTER TABLE sp_rollout ADD COLUMN is_dynamic BOOLEAN; +ALTER TABLE sp_rolloutgroup ADD COLUMN is_dynamic BOOLEAN NOT NULL DEFAULT false; + +UPDATE sp_rollout SET weight = 1000 WHERE weight IS NULL; +UPDATE sp_action SET weight = 1000 WHERE weight IS NULL; +UPDATE sp_target_filter_query SET auto_assign_weight = 1000 WHERE auto_assign_weight IS NULL; +ALTER TABLE sp_rollout ALTER COLUMN weight SET NOT NULL; +ALTER TABLE sp_action ALTER COLUMN weight SET NOT NULL; +ALTER TABLE sp_target_filter_query ALTER COLUMN auto_assign_weight SET NOT NULL; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_28__add_dynamic_rollout___H2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_28__add_dynamic_rollout___H2.sql new file mode 100644 index 000000000..f392ccd87 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_28__add_dynamic_rollout___H2.sql @@ -0,0 +1,9 @@ +ALTER TABLE sp_rollout ADD COLUMN is_dynamic BOOLEAN; +ALTER TABLE sp_rolloutgroup ADD COLUMN is_dynamic BOOLEAN NOT NULL DEFAULT false; + +UPDATE sp_rollout SET weight = 1000 WHERE weight IS NULL; +UPDATE sp_action SET weight = 1000 WHERE weight IS NULL; +UPDATE sp_target_filter_query SET auto_assign_weight = 1000 WHERE auto_assign_weight IS NULL; +ALTER TABLE sp_rollout ALTER COLUMN weight INT NOT NULL; +ALTER TABLE sp_action ALTER COLUMN weight INT NOT NULL; +ALTER TABLE sp_target_filter_query ALTER COLUMN auto_assign_weight INT NOT NULL; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_28__add_dynamic_rollout___MYSQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_28__add_dynamic_rollout___MYSQL.sql new file mode 100644 index 000000000..030727a57 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_28__add_dynamic_rollout___MYSQL.sql @@ -0,0 +1,9 @@ +ALTER TABLE sp_rollout ADD COLUMN is_dynamic BOOLEAN; +ALTER TABLE sp_rolloutgroup ADD COLUMN is_dynamic BOOLEAN NOT NULL DEFAULT false; + +UPDATE sp_rollout SET weight = 1000 WHERE weight IS NULL; +UPDATE sp_action SET weight = 1000 WHERE weight IS NULL; +UPDATE sp_target_filter_query SET auto_assign_weight = 1000 WHERE auto_assign_weight IS NULL; +ALTER TABLE sp_rollout MODIFY COLUMN weight INT NOT NULL; +ALTER TABLE sp_action MODIFY COLUMN weight INT NOT NULL; +ALTER TABLE sp_target_filter_query MODIFY COLUMN auto_assign_weight INT NOT NULL; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_28__add_dynamic_rollout___POSTGRESQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_28__add_dynamic_rollout___POSTGRESQL.sql new file mode 100644 index 000000000..de6a6f14f --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_28__add_dynamic_rollout___POSTGRESQL.sql @@ -0,0 +1,9 @@ +ALTER TABLE sp_rollout ADD COLUMN is_dynamic BOOLEAN; +ALTER TABLE sp_rolloutgroup ADD COLUMN is_dynamic BOOLEAN NOT NULL DEFAULT false; + +UPDATE sp_rollout SET weight = 1000 WHERE weight IS NULL; +UPDATE sp_action SET weight = 1000 WHERE weight IS NULL; +UPDATE sp_target_filter_query SET auto_assign_weight = 1000 WHERE auto_assign_weight IS NULL; +ALTER TABLE sp_rollout ALTER COLUMN weight SET NOT NULL; +ALTER TABLE sp_action ALTER COLUMN weight SET NOT NULL; +ALTER TABLE sp_target_filter_query ALTER COLUMN auto_assign_weight SET NOT NULL; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_28__add_dynamic_rollout___SQL_SERVER.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_28__add_dynamic_rollout___SQL_SERVER.sql new file mode 100644 index 000000000..a22a7f997 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_28__add_dynamic_rollout___SQL_SERVER.sql @@ -0,0 +1,9 @@ +ALTER TABLE sp_rollout ADD is_dynamic BIT; +ALTER TABLE sp_rolloutgroup ADD is_dynamic BIT NOT NULL DEFAULT 0; + +UPDATE sp_rollout SET weight = 1000 WHERE weight IS NULL; +UPDATE sp_action SET weight = 1000 WHERE weight IS NULL; +UPDATE sp_target_filter_query SET auto_assign_weight = 1000 WHERE auto_assign_weight IS NULL; +ALTER TABLE sp_rollout ALTER COLUMN weight INT NOT NULL; +ALTER TABLE sp_action ALTER COLUMN weight INT NOT NULL; +ALTER TABLE sp_target_filter_query ALTER COLUMN auto_assign_weight INT NOT NULL; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteTenantAwareEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteTenantAwareEventTest.java index ad88c0f42..eaa3a7494 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteTenantAwareEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteTenantAwareEventTest.java @@ -106,6 +106,7 @@ public class RemoteTenantAwareEventTest extends AbstractRemoteEventTest { generateAction.setDistributionSet(dsA); generateAction.setStatus(Status.RUNNING); generateAction.setInitiatedBy(tenantAware.getCurrentUsername()); + generateAction.setWeight(1000); final Action action = actionRepository.save(generateAction); @@ -132,6 +133,7 @@ public class RemoteTenantAwareEventTest extends AbstractRemoteEventTest { generateAction.setDistributionSet(dsA); generateAction.setStatus(Status.RUNNING); generateAction.setInitiatedBy(tenantAware.getCurrentUsername()); + generateAction.setWeight(1000); final Action action = actionRepository.save(generateAction); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/ActionEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/ActionEventTest.java index fd871ca6c..1522cdea9 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/ActionEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/ActionEventTest.java @@ -11,6 +11,7 @@ package org.eclipse.hawkbit.repository.event.remote.entity; import static org.assertj.core.api.Assertions.assertThat; +import net.bytebuddy.agent.builder.AgentBuilder; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; @@ -86,6 +87,7 @@ public class ActionEventTest extends AbstractRemoteEntityEventTest { generateAction.setDistributionSet(distributionSet); generateAction.setStatus(Status.RUNNING); generateAction.setInitiatedBy(tenantAware.getCurrentUsername()); + generateAction.setWeight(1000); return actionRepository.save(generateAction); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java index 83e862e7d..6784023fa 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java @@ -19,6 +19,9 @@ import javax.persistence.PersistenceContext; import org.assertj.core.api.Assertions; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.jpa.model.JpaAction; +import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; +import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository; import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository; import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetRepository; @@ -40,6 +43,7 @@ import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetTag; import org.eclipse.hawkbit.repository.model.DistributionSetTagAssignmentResult; 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; import org.eclipse.hawkbit.repository.model.TargetTagAssignmentResult; @@ -52,6 +56,7 @@ import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; import org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration; +import org.springframework.data.domain.Page; import org.springframework.orm.jpa.vendor.Database; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; @@ -59,6 +64,8 @@ import org.springframework.transaction.annotation.Transactional; import com.google.common.collect.Lists; +import static org.assertj.core.api.Assertions.assertThat; + @ContextConfiguration(classes = { RepositoryApplicationConfiguration.class, TestConfiguration.class, TestSupportBinderAutoConfiguration.class }) @@ -159,4 +166,42 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest targets.stream().map(Target::getControllerId).collect(Collectors.toList()), type.getId()); } + protected void assertRollout(final Rollout rollout, final boolean dynamic, final Rollout.RolloutStatus status, final int groupCreated, final long totalTargets) { + final Rollout refreshed = refresh(rollout); + assertThat(refreshed.isDynamic()).isEqualTo(dynamic); + assertThat(refreshed.getStatus()).isEqualTo(status); + assertThat(refreshed.getRolloutGroupsCreated()).isEqualTo(groupCreated); + assertThat(refreshed.getTotalTargets()).isEqualTo(totalTargets); + } + + protected void assertGroup(final RolloutGroup group, final boolean dynamic, final RolloutGroup.RolloutGroupStatus status, final long totalTargets) { + final RolloutGroup refreshed = refresh(group); + assertThat(refreshed.isDynamic()).isEqualTo(dynamic); + assertThat(refreshed.getStatus()).isEqualTo(status); + assertThat(refreshed.getTotalTargets()).isEqualTo(totalTargets); + } + + protected Page assertAndGetRunning(final Rollout rollout, final int count) { + final Page running = actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), Action.Status.RUNNING); + assertThat(running.getTotalElements()).isEqualTo(count); + return running; + } + + protected void assertScheduled(final Rollout rollout, final int count) { + final Page running = actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), Action.Status.SCHEDULED); + assertThat(running.getTotalElements()).isEqualTo(count); + } + + protected void finishAction(final Action action) { + controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(action.getId()).status(Action.Status.FINISHED)); + } + + private JpaRollout refresh(final Rollout rollout) { + return rolloutRepository.findById(rollout.getId()).get(); + } + + protected JpaRolloutGroup refresh(final RolloutGroup group) { + return rolloutGroupRepository.findById(group.getId()).get(); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignCheckerIntTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignCheckerIntTest.java index 322c2bcb6..72fe63161 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignCheckerIntTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignCheckerIntTest.java @@ -440,7 +440,7 @@ class AutoAssignCheckerIntTest extends AbstractJpaIntegrationTest { final List actions = deploymentManagement.findActionsAll(PAGE).getContent(); assertThat(actions).hasSize(amountOfTargets); - assertThat(actions).allMatch(action -> !action.getWeight().isPresent()); + assertThat(actions).allMatch(action -> action.getWeight().isPresent()); } @Test diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanupTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanupTest.java index ed42c57f0..fde6fe3ed 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanupTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanupTest.java @@ -115,6 +115,7 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest { assertThat(actionRepository.count()).isEqualTo(3); + waitNextMillis(); autoActionCleanup.run(); assertThat(actionRepository.count()).isEqualTo(1); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ControllerManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ControllerManagementTest.java index 35125f7d9..b1e7531f8 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ControllerManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ControllerManagementTest.java @@ -1101,6 +1101,9 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId) .status(Action.Status.RUNNING).occurredAt(System.currentTimeMillis()) .messages(Lists.newArrayList("proceeding message 1"))); + + final long createTime = System.currentTimeMillis(); + waitNextMillis(); controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId) .status(Action.Status.RUNNING).occurredAt(System.currentTimeMillis()) .messages(Lists.newArrayList("proceeding message 2"))); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DeploymentManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DeploymentManagementTest.java index 338e6d285..e3ed5cbd7 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DeploymentManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DeploymentManagementTest.java @@ -972,18 +972,14 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest { @Test @Description("An assignment request containing a weight causes an error when multi assignment in disabled.") - @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = DistributionSetCreatedEvent.class, count = 1), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) - void weightNotAllowedWhenMultiAssignmentModeNotEnabled() { + void weightAllowedWhenMultiAssignmentModeNotEnabled() { final String targetId = testdataFactory.createTarget().getControllerId(); final Long dsId = testdataFactory.createDistributionSet().getId(); final DeploymentRequest assignWithoutWeight = DeploymentManagement.deploymentRequest(targetId, dsId) .setWeight(456).build(); - Assertions.assertThatExceptionOfType(MultiAssignmentIsNotEnabledException.class).isThrownBy( - () -> deploymentManagement.assignDistributionSets(Collections.singletonList(assignWithoutWeight))); + deploymentManagement.assignDistributionSets(Collections.singletonList(assignWithoutWeight)); } @Test diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementFlowTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementFlowTest.java new file mode 100644 index 000000000..22b937624 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementFlowTest.java @@ -0,0 +1,205 @@ +/** + * Copyright (c) 2023 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.management; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; +import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; +import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; +import org.eclipse.hawkbit.repository.model.DistributionSet; +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.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Junit tests for RolloutManagement. + */ +@Feature("Component Tests - Repository") +@Story("Rollout Management (Flow)") +class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { + + @BeforeEach + void reset() { + this.approvalStrategy.setApprovalNeeded(false); + } + + @Test + @Description("Verifies a simple rollout flow") + void rolloutFlow() { + final String rolloutName = "rollout-std"; + final int amountGroups = 5; // static only + final String targetPrefix = "controller-rollout-std-"; + final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName); + + testdataFactory.createTargets(targetPrefix, 0, amountGroups * 3); + final Rollout rollout = testdataFactory.createRolloutByVariables(rolloutName, rolloutName, amountGroups, + "controllerid==" + targetPrefix + "*", distributionSet, "60", "30", false, false); + final List groups = rolloutGroupManagement.findByRollout( + new OffsetBasedPageRequest(0, amountGroups + 10, Sort.by(Direction.ASC, "id")), + rollout.getId()).getContent(); + + // add 2 targets not to be included + testdataFactory.createTargets(targetPrefix, amountGroups * 3, 2); + // start rollout + rolloutManagement.start(rollout.getId()); + + // handleStartingRollout (no handleRunning called yet) + rolloutHandler.handleAll(); + assertRollout(rollout, false, RolloutStatus.RUNNING, amountGroups, amountGroups * 3); + for (int i = 0; i < amountGroups; i++) { + assertGroup(groups.get(i), false, i == 0 ? RolloutGroupStatus.RUNNING : RolloutGroupStatus.SCHEDULED, 3); + } + + // execute groups (without on of the last) + assertThat(refresh(groups.get(0)).getStatus()).isEqualTo(RolloutGroupStatus.RUNNING); + for (int i = 0; i < amountGroups; i++) { + if (i + 1 != amountGroups) { + assertThat(refresh(groups.get(i + 1)).getStatus()).isEqualTo(RolloutGroupStatus.SCHEDULED); + } + assertAndGetRunning(rollout, 3) + .stream() + .filter(action -> !(targetPrefix + (amountGroups * 3 - 1)).equals(action.getTarget().getControllerId())) + .forEach(this::finishAction); + rolloutHandler.handleAll(); + assertThat(refresh(groups.get(i)).getStatus()).isEqualTo(i + 1 == amountGroups ? RolloutGroupStatus.RUNNING : RolloutGroupStatus.FINISHED); + if (i + 1 != amountGroups) { + assertThat(refresh(groups.get(i + 1)).getStatus()).isEqualTo(RolloutGroupStatus.RUNNING); + } + } + + rolloutManagement.pauseRollout(rollout.getId()); + rolloutHandler.handleAll(); + assertRollout(rollout, false, RolloutStatus.PAUSED, amountGroups, amountGroups * 3); + assertAndGetRunning(rollout, 1); // keep running + + rolloutManagement.resumeRollout(rollout.getId()); + rolloutHandler.handleAll(); + assertRollout(rollout, false, RolloutStatus.RUNNING, amountGroups, amountGroups * 3); + assertAndGetRunning(rollout, 1); // keep running + } + + @Test + @Description("Verifies a simple dynamic rollout flow") + void dynamicRolloutFlow() { + final String rolloutName = "dynamic-rollout-std"; + final int amountGroups = 5; // static only + final String targetPrefix = "controller-dynamic-rollout-std-"; + final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName); + + testdataFactory.createTargets(targetPrefix, 0, amountGroups * 3); + final Rollout rollout = testdataFactory.createRolloutByVariables(rolloutName, rolloutName, amountGroups, + "controllerid==" + targetPrefix + "*", distributionSet, "60", "30", false, true); + + // rollout is READY + assertRollout(rollout, true, RolloutStatus.READY, amountGroups + 1, amountGroups * 3); + List groups = rolloutGroupManagement.findByRollout( + new OffsetBasedPageRequest(0, amountGroups + 10, Sort.by(Direction.ASC, "id")), + rollout.getId()).getContent(); + final RolloutGroup dynamic1 = groups.get(amountGroups); + assertRollout(rollout, true, RolloutStatus.READY, amountGroups + 1, amountGroups * 3); // + dynamic + for (int i = 0; i < amountGroups; i++) { + assertGroup(groups.get(i), false, RolloutGroupStatus.READY, 3); + } + assertGroup(dynamic1, true, RolloutGroupStatus.READY, 0); + + // add 2 targets for the first dynamic group + testdataFactory.createTargets(targetPrefix, amountGroups * 3, 2); + // start rollout + rolloutManagement.start(rollout.getId()); + + // handleStartingRollout (no handleRunning called yet) + rolloutHandler.handleAll(); + assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 1, amountGroups * 3); + for (int i = 0; i < amountGroups; i++) { + assertGroup(groups.get(i), false, i == 0 ? RolloutGroupStatus.RUNNING : RolloutGroupStatus.SCHEDULED, 3); + } + assertGroup(dynamic1, true, RolloutGroupStatus.SCHEDULED, 0); + + // execute statics (without on of the last) which start dynamic + assertThat(refresh(groups.get(0)).getStatus()).isEqualTo(RolloutGroupStatus.RUNNING); + for (int i = 0; i < amountGroups; i++) { + assertThat(refresh(groups.get(i + 1)).getStatus()).isEqualTo(RolloutGroupStatus.SCHEDULED); + assertAndGetRunning(rollout, 3) + .stream() + .filter(action -> !(targetPrefix + (amountGroups * 3 - 1)).equals(action.getTarget().getControllerId())) + .forEach(this::finishAction); + rolloutHandler.handleAll(); + assertThat(refresh(groups.get(i)).getStatus()).isEqualTo(i + 1 == amountGroups ? RolloutGroupStatus.RUNNING : RolloutGroupStatus.FINISHED); + assertThat(refresh(i + 1 == amountGroups ? dynamic1 : groups.get(i + 1)).getStatus()).isEqualTo(RolloutGroupStatus.RUNNING); // on last round check dynamic + } + + // partially fill the first dynamic (it is running and now create actions for 2 targets) + rolloutHandler.handleAll(); + assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 1, amountGroups * 3 + 2); + assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 2); + + // fill first and create second + testdataFactory.createTargets(targetPrefix, amountGroups * 3 + 2, 2); + rolloutHandler.handleAll(); // fill first dynamic group and create a new dynamic2 + assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 3); + assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 3); + groups = rolloutGroupManagement.findByRollout( + new OffsetBasedPageRequest(0, amountGroups + 10, Sort.by(Direction.ASC, "id")), + rollout.getId()).getContent(); + final RolloutGroup dynamic2 = groups.get(amountGroups + 1); + assertGroup(dynamic2, true, RolloutGroupStatus.SCHEDULED, 0); + + // create scheduled actions for the dynamic2 + rolloutHandler.handleAll(); + assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 3); + assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 3); + assertGroup(dynamic2, true, RolloutGroupStatus.SCHEDULED, 0); + assertAndGetRunning(rollout, 4); // one from the last static group and 3 from the first dynamic + assertScheduled(rollout, 0); + + // executes last from static and dynamic1 without 1 target + assertAndGetRunning(rollout, 4)// one from the last static and 3 for the first dynamic + .stream() + // remove the last assigned to dynamic1 - it could be amountGroups * 3 + 2 or bigger by id + .filter(action -> Integer.parseInt(action.getTarget().getControllerId().substring(targetPrefix.length())) < amountGroups * 3 + 2) + .forEach(this::finishAction); + assertAndGetRunning(rollout, 1); // remains on in the first dynamic + + + rolloutHandler.handleAll(); + assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 3); + assertGroup(groups.get(amountGroups - 1), false, RolloutGroupStatus.FINISHED, 3); + assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 3); + assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 0); + + rolloutHandler.handleAll(); // add 1 action to now running second dynamic + assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 4); + assertAndGetRunning(rollout, 2); + assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 1); + + testdataFactory.createTargets(targetPrefix, amountGroups * 3 + 4, 1); + rolloutManagement.pauseRollout(rollout.getId()); + rolloutHandler.handleAll(); + assertRollout(rollout, true, RolloutStatus.PAUSED, amountGroups + 2, amountGroups * 3 + 4); + assertAndGetRunning(rollout, 2); + assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 1); // no new assignment + + rolloutManagement.resumeRollout(rollout.getId()); + rolloutHandler.handleAll(); + assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 5); + assertAndGetRunning(rollout, 3); + assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 2); // assign the target created when paused + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java index 1d0297e3d..b6874b1ab 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java @@ -440,11 +440,6 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { } - private void finishAction(final Action action) { - controllerManagement - .addUpdateActionStatus(entityFactory.actionStatus().create(action.getId()).status(Status.FINISHED)); - } - @Test @Description("Verifying that the error handling action of a group is executed to pause the current rollout") void checkErrorHitOfGroupCallsErrorActionToPauseTheRollout() { @@ -1432,7 +1427,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { final RolloutGroupCreate group1 = entityFactory.rolloutGroup().create().conditions(conditions).name("group1") .targetPercentage(50.0F); final RolloutGroupCreate group2 = entityFactory.rolloutGroup().create().conditions(conditions).name("group2") - .targetPercentage(100.0F); + .targetPercentage(50.0F); // group1 exceeds the quota assertThatExceptionOfType(AssignmentQuotaExceededException.class).isThrownBy(() -> rolloutManagement.create( @@ -1456,9 +1451,9 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { final RolloutGroupCreate group5 = entityFactory.rolloutGroup().create().conditions(conditions).name("group5") .targetPercentage(33.3F); final RolloutGroupCreate group6 = entityFactory.rolloutGroup().create().conditions(conditions).name("group6") - .targetPercentage(66.6F); + .targetPercentage(33.3F); final RolloutGroupCreate group7 = entityFactory.rolloutGroup().create().conditions(conditions).name("group7") - .targetPercentage(100.0F); + .targetPercentage(33.3F); // should work fine assertThat(rolloutManagement.create( @@ -1531,17 +1526,17 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { final int amountTargetsInGroup1 = 10; final int percentTargetsInGroup1 = 100; - final int amountTargetsInGroup1and2 = 20; + final int amountTargetsInGroup2and3 = 20; final int percentTargetsInGroup2 = 20; - final int percentTargetsInGroup3 = 100; + final int percentTargetsInGroup3 = 80; final int countTargetsInGroup2 = (int) Math - .ceil((double) percentTargetsInGroup2 / 100 * amountTargetsInGroup1and2); - final int countTargetsInGroup3 = amountTargetsInGroup1and2 - countTargetsInGroup2; + .ceil((double) percentTargetsInGroup2 / 100 * amountTargetsInGroup2and3); + final int countTargetsInGroup3 = amountTargetsInGroup2and3 - countTargetsInGroup2; final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults().build(); // Generate Targets for group 2 and 3 and generate the Rollout - final RolloutCreate rolloutcreate = generateTargetsAndRollout(rolloutName, amountTargetsInGroup1and2); + final RolloutCreate rolloutcreate = generateTargetsAndRollout(rolloutName, amountTargetsInGroup2and3); // Generate Targets for group 1 testdataFactory.createTargets(amountTargetsInGroup1, rolloutName + "-gr1-", rolloutName); @@ -1568,7 +1563,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { myRollout = getRollout(myRollout.getId()); assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.READY); - assertThat(myRollout.getTotalTargets()).isEqualTo(amountTargetsInGroup1and2 + amountTargetsInGroup1); + assertThat(myRollout.getTotalTargets()).isEqualTo(amountTargetsInGroup2and3 + amountTargetsInGroup1); final List groups = rolloutGroupManagement.findByRollout(PAGE, myRollout.getId()).getContent(); @@ -1979,11 +1974,10 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Creating a rollout with a weight causes an error when multi assignment in disabled.") - void weightNotAllowedWhenMultiAssignmentModeNotEnabled() { - Assertions.assertThatExceptionOfType(MultiAssignmentIsNotEnabledException.class) - .isThrownBy(() -> testdataFactory.createSimpleTestRolloutWithTargetsAndDistributionSet(10, 10, 2, "50", + void weightAllowedWhenMultiAssignmentModeNotEnabled() { + testdataFactory.createSimpleTestRolloutWithTargetsAndDistributionSet(10, 10, 2, "50", "80", - ActionType.FORCED, 66)); + ActionType.FORCED, 66); } @Test @@ -2028,7 +2022,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Rollout can be created without weight in single assignment and be started in multi assignment") - void createInSingleStartInMultiassigMode() { + void createInSingleStartInMultiassignMode() { final int amountOfTargets = 5; final Long rolloutId = testdataFactory.createSimpleTestRolloutWithTargetsAndDistributionSet(amountOfTargets, 2, amountOfTargets, @@ -2038,7 +2032,8 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { rolloutManagement.start(rolloutId); rolloutHandler.handleAll(); final List actions = deploymentManagement.findActionsAll(PAGE).getContent(); - assertThat(actions).hasSize(amountOfTargets).allMatch(action -> !action.getWeight().isPresent()); + // wight replaced with default + assertThat(actions).hasSize(5).allMatch(action -> action.getWeight().isPresent()); } @Test @@ -2319,4 +2314,66 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { .isThrownBy(() -> rolloutManagement.triggerNextGroup(createdRollout.getId())) .withMessageContaining(errorMessage); } + + /** + * Tests static assignment aspects of the dynamic group assignment filters. + */ + @Test + @Description("Dynamic group doesn't override newer static group assignments") + public void dynamicGroupDoesntOverrideItsOrNewerStaticGroups() { + final int amountGroups = 1; // static only + final String targetPrefix = "controller-dynamic-rollout-"; + final DistributionSet distributionSet = testdataFactory.createDistributionSet("ds"); + + 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); + rolloutManagement.start(dynamicRollout.getId()); + rolloutHandler.handleAll(); + assertRollout(dynamicRollout, true, RolloutStatus.RUNNING, amountGroups + 1, amountGroups * 2); + final List dynamicGroups = rolloutGroupManagement.findByRollout( + new OffsetBasedPageRequest(0, amountGroups + 10, Sort.by(Direction.ASC, "id")), + dynamicRollout.getId()).getContent(); + for (int i = 0; i < dynamicGroups.size(); i++) { + final RolloutGroup group = dynamicGroups.get(i); + if (i + 1 == dynamicGroups.size()) { + assertGroup(group, true, RolloutGroupStatus.SCHEDULED, 0); + } else { + assertGroup(group, false, RolloutGroupStatus.RUNNING, 2); + } + } + assertAndGetRunning(dynamicRollout, 2).forEach(this::finishAction); + rolloutHandler.handleAll(); + for (int i = 0; i < dynamicGroups.size(); i++) { + final RolloutGroup group = dynamicGroups.get(i); + if (i + 1 == dynamicGroups.size()) { + assertGroup(group, true, RolloutGroupStatus.RUNNING, 0); + } else { + assertGroup(group, false, RolloutGroupStatus.FINISHED, 2); + } + } + assertAndGetRunning(dynamicRollout, 0); + rolloutHandler.handleAll(); + // NB: asserts that dynamic group doesn't get from its static groups (already finished action targets) + assertGroup(dynamicGroups.get(dynamicGroups.size() - 1), true, RolloutGroupStatus.RUNNING, 0); + assertAndGetRunning(dynamicRollout, 0); + rolloutManagement.pauseRollout(dynamicRollout.getId()); + rolloutHandler.handleAll(); + + 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); + rolloutManagement.start(staticRollout.getId()); + rolloutHandler.handleAll(); + assertRollout(staticRollout, false, RolloutStatus.RUNNING, amountGroups, amountGroups * 3); + final List staticGroups = rolloutGroupManagement.findByRollout( + new OffsetBasedPageRequest(0, amountGroups + 10, Sort.by(Direction.ASC, "id")), + staticRollout.getId()).getContent(); + staticGroups.forEach(group -> assertGroup(group, false, RolloutGroupStatus.RUNNING, 3)); + + rolloutManagement.resumeRollout(dynamicRollout.getId()); + rolloutHandler.handleAll(); // resume, do not get last devices (they are assigned to a newer group, nevertheless newer is with bigger weight + assertGroup(dynamicGroups.get(dynamicGroups.size() - 1), true, RolloutGroupStatus.RUNNING, 0); + assertAndGetRunning(dynamicRollout, 0); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetFilterQueryManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetFilterQueryManagementTest.java index 54ad3ca98..34cab47cc 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetFilterQueryManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetFilterQueryManagementTest.java @@ -463,17 +463,15 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest @Test @Description("Creating or updating a target filter query with autoassignment with a weight causes an error when multi assignment in disabled.") - public void weightNotAllowedWhenMultiAssignmentModeNotEnabled() { + public void weightAllowedWhenMultiAssignmentModeNotEnabled() { final DistributionSet ds = testdataFactory.createDistributionSet(); final Long filterId = targetFilterQueryManagement .create(entityFactory.targetFilterQuery().create().name("a").query("name==*")).getId(); - Assertions.assertThatExceptionOfType(MultiAssignmentIsNotEnabledException.class) - .isThrownBy(() -> targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create() - .name("b").query("name==*").autoAssignDistributionSet(ds).autoAssignWeight(342))); - Assertions.assertThatExceptionOfType(MultiAssignmentIsNotEnabledException.class) - .isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS( - entityFactory.targetFilterQuery().updateAutoAssign(filterId).ds(ds.getId()).weight(343))); + targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create() + .name("b").query("name==*").autoAssignDistributionSet(ds).autoAssignWeight(342)); + targetFilterQueryManagement.updateAutoAssignDS( + entityFactory.targetFilterQuery().updateAutoAssign(filterId).ds(ds.getId()).weight(343)); } @Test diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetManagementTest.java index 086503947..fc5097775 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetManagementTest.java @@ -53,13 +53,17 @@ import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.TenantNotExistException; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; +import org.eclipse.hawkbit.repository.jpa.model.JpaAction; +import org.eclipse.hawkbit.repository.jpa.model.JpaAction_; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.MetaData; import org.eclipse.hawkbit.repository.model.NamedEntity; +import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Tag; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetMetadata; @@ -1328,6 +1332,65 @@ class TargetManagementTest extends AbstractJpaIntegrationTest { "name==*")).isFalse(); } + /** + * Tests action based aspects of the dynamic group assignment filters. + */ + @Test + @Description("Target matches filter no active action with ge weight.") + void findByNotInGEGroupAndNotInActiveActionGEWeightOrInRolloutAndTargetFilterQueryAndCompatibleAndUpdatable() { + final String targetPrefix = "dyn_action_filter_"; + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); + final List targets = testdataFactory.createTargets(targetPrefix, 9); + final Rollout rolloutOlder = testdataFactory.createRollout(); + final Rollout rollout = testdataFactory.createRollout(); + final Rollout rolloutNewer = testdataFactory.createRollout(); + + // old ro with less weight - match + createAction(targets.get(0), rolloutOlder, 0, Status.RUNNING, distributionSet); + // old ro with less weight - match + createAction(targets.get(1), rolloutOlder, 5, Status.SCHEDULED, distributionSet); + // old ro with equal weight - match + createAction(targets.get(2), rolloutOlder, 10, Status.RUNNING, distributionSet); + // old ro with BIGGER weight - doesn't match + createAction(targets.get(3), rolloutOlder, 20, Status.WAIT_FOR_CONFIRMATION, distributionSet); + // same ro - doesn't match + createAction(targets.get(4), rollout, 10, Status.RUNNING, distributionSet); + // new ro with less weight - match + createAction(targets.get(5), rolloutNewer, 0, Status.RUNNING, distributionSet); + // new ro with less weight - match + createAction(targets.get(6), rolloutNewer, 5, Status.WARNING, distributionSet); + // NEW ro with EQUAL weight - doesn't match + createAction(targets.get(7), rolloutNewer, 10, Status.RUNNING, distributionSet); + // new ro with BIGGER weight - doesn't match + createAction(targets.get(8), rolloutNewer, 20, Status.DOWNLOADED, distributionSet); + + final Slice matching = targetManagement.findByNotInGEGroupAndNotInActiveActionGEWeightOrInRolloutAndTargetFilterQueryAndCompatibleAndUpdatable( + PAGE, rollout.getId(), 10, Long.MAX_VALUE,"controllerid==dyn_action_filter_*", distributionSet.getType()); + + assertThat(matching.getNumberOfElements()).isEqualTo(5); + assertThat(matching.stream() + .map(Target::getControllerId) + .map(s -> s.substring(targetPrefix.length())) + .map(Integer::parseInt) + .sorted() + .toList()).isEqualTo(List.of(0, 1, 2, 5, 6)); + } + private void createAction(final Target target, final Rollout rollout, final Integer weight, final Action.Status status, final DistributionSet distributionSet) { + final JpaAction action = new JpaAction(); + action.setActionType(Action.ActionType.FORCED); + action.setTarget(target); + action.setInitiatedBy("test"); + if (rollout != null) { + action.setRollout(rollout); + } + if (weight != null) { + action.setWeight(weight); + } + action.setStatus(status); + action.setDistributionSet(distributionSet); + actionRepository.save(action); + } + @Test @Description("Target matches filter for not existing DS.") void matchesFilterDsNotExists() { diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java index 84ca4d9bf..1b79887ce 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java @@ -513,4 +513,15 @@ public abstract class AbstractIntegrationTest { .getConfigurationValue(TenantConfigurationKey.USER_CONFIRMATION_ENABLED, Boolean.class).getValue(); } + // ensure that next action will get current time millis AFTER got from the previous + protected void waitNextMillis() { + final long createTime = System.currentTimeMillis(); + while (System.currentTimeMillis() == createTime) { + try { + Thread.sleep(1); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } } diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java index 1d210172d..f52a65ec0 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java @@ -287,7 +287,7 @@ public class TestdataFactory { * for {@link SoftwareModule}s and {@link DistributionSet}s name, * vendor and description. * @param tags - * {@link DistributionSet#getTags()} + * DistributionSet tags * * @return {@link DistributionSet} entity. */ @@ -397,7 +397,7 @@ public class TestdataFactory { * {@link SoftwareModule#getVersion()} extended by a random * number.updat * @param tags - * {@link DistributionSet#getTags()} + * DistributionSet tags * * @return {@link DistributionSet} entity. */ @@ -909,10 +909,13 @@ public class TestdataFactory { } public List createTargets(final String prefix, final int number) { + return createTargets(prefix, 0, number); + } + public List createTargets(final String prefix, final int offset, final int number) { final List targets = Lists.newArrayListWithExpectedSize(number); for (int i = 0; i < number; i++) { - targets.add(entityFactory.target().create().controllerId(prefix + i)); + targets.add(entityFactory.target().create().controllerId(prefix + (offset + i))); } return createTargets(targets); @@ -1192,6 +1195,21 @@ public class TestdataFactory { successCondition, errorCondition, Action.ActionType.FORCED, null, confirmationRequired); } + 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 boolean dynamic) { + return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet, + successCondition, errorCondition, Action.ActionType.FORCED, null, confirmationRequired, dynamic); + } + + 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 Integer weight, final boolean confirmationRequired) { + return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet, + successCondition, errorCondition, actionType, weight, confirmationRequired, false); + } /** * Creates rollout based on given parameters. * @@ -1216,12 +1234,13 @@ public class TestdataFactory { * @param confirmationRequired * if the confirmation is required (considered with confirmation flow * active) + * @param dynamic is dynamic * @return created {@link Rollout} */ 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 Integer weight, final boolean confirmationRequired) { + final Integer weight, final boolean confirmationRequired, final boolean dynamic) { final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults() .successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition) .errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition) @@ -1229,7 +1248,8 @@ public class TestdataFactory { final Rollout rollout = rolloutManagement.create( entityFactory.rollout().create().name(rolloutName).description(rolloutDescription) - .targetFilterQuery(filterQuery).set(distributionSet).actionType(actionType).weight(weight), + .targetFilterQuery(filterQuery).set(distributionSet).actionType(actionType).weight(weight) + .dynamic(dynamic), groupSize, confirmationRequired, conditions); // Run here, because Scheduler is disabled during tests @@ -1300,7 +1320,7 @@ public class TestdataFactory { * the amount of targets used for the rollout * @param amountOtherTargets * amount of other targets not included in the rollout - * @param groupSize + * @param amountGroups * the size of the rollout group * @param successCondition * success condition diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java index 3d8d4d402..999028bcc 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java @@ -71,6 +71,10 @@ public class MgmtRolloutResponseBody extends MgmtNamedEntity { @Schema(example = "400") private Integer weight; + @JsonProperty + @Schema(example = "true") + private boolean dynamic; + @JsonProperty @Schema(example = "Approved remark.") private String approvalRemark; @@ -171,6 +175,14 @@ public class MgmtRolloutResponseBody extends MgmtNamedEntity { return weight; } + public void setDynamic(final boolean dynamic) { + this.dynamic = dynamic; + } + + public boolean isDynamic() { + return dynamic; + } + public void setTotalGroups(final Integer totalGroups) { this.totalGroups = totalGroups; } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java index 74692acc3..464ce0aff 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java @@ -47,6 +47,10 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt @Schema(example = "400") private Integer weight; + @JsonProperty(required = false) + @Schema(example = "true") + private boolean dynamic; + @JsonProperty(required = false) @Schema(example = "false") private Boolean confirmationRequired; @@ -175,6 +179,21 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt this.weight = weight; } + /** + * @return if the {@link Rollout} shall be dynamic + */ + public boolean isDynamic() { + return dynamic; + } + + /** + * @param dynamic + * is the {@link Rollout} shall be dynamic + */ + public void setDynamic(final boolean dynamic) { + this.dynamic = dynamic; + } + /** * Only considered if confirmation flow active * 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 7ed3d683d..47555db64 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 @@ -79,6 +79,7 @@ final class MgmtRolloutMapper { body.setLastModifiedBy(rollout.getLastModifiedBy()); body.setName(rollout.getName()); body.setRolloutId(rollout.getId()); + body.setDynamic(rollout.isDynamic()); body.setTargetFilterQuery(rollout.getTargetFilterQuery()); body.setDistributionSetId(rollout.getDistributionSet().getId()); body.setStatus(rollout.getStatus().toString().toLowerCase()); @@ -126,6 +127,7 @@ final class MgmtRolloutMapper { final DistributionSet distributionSet) { return entityFactory.rollout().create().name(restRequest.getName()).description(restRequest.getDescription()) + .dynamic(restRequest.isDynamic()) .set(distributionSet).targetFilterQuery(restRequest.getTargetFilterQuery()) .actionType(MgmtRestModelMapper.convertActionType(restRequest.getType())) .forcedTime(restRequest.getForcetime()).startAt(restRequest.getStartAt()) diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java index 43e02b5a5..6a1a8a8fd 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java @@ -1516,8 +1516,7 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", dsId).content(bodyValide.toString()) .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.multiassignmentNotEnabled"))); + .andExpect(status().isOk()); enableMultiAssignments(); mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", dsId).content(bodyInvalide.toString()) .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) @@ -1528,7 +1527,7 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr .andExpect(status().isOk()); final List actions = deploymentManagement.findActionsAll(PAGE).get().collect(Collectors.toList()); - assertThat(actions).size().isEqualTo(1); + assertThat(actions).size().isEqualTo(2); assertThat(actions.get(0).getWeight()).get().isEqualTo(weight); } 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 0986f6b77..9929dc9c8 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 @@ -1364,7 +1364,7 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest { } @Test - @Description("A rollout create request containing a weight is only accepted when weight is valid and multi assignment is on.") + @Description("A rollout create request containing a weight is always accepted when weight is valid.") void weightValidation() throws Exception { testdataFactory.createTargets(4, "rollout", "description"); final Long dsId = testdataFactory.createDistributionSet().getId(); @@ -1375,22 +1375,23 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest { null, null); final String valideWeightRequest = JsonBuilder.rollout("withWeight", "d", 2, dsId, "id==rollout*", new RolloutGroupConditionBuilder().withDefaults().build(), null, null, weight, null, null, null); + final String valideWeightRequestMultiAssignment = JsonBuilder.rollout("withWeightMultiAssignment", "d", 2, dsId, "id==rollout*", + new RolloutGroupConditionBuilder().withDefaults().build(), null, null, weight, null, null, null); mvc.perform(post("/rest/v1/rollouts").content(valideWeightRequest).contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.multiassignmentNotEnabled"))); + .andExpect(status().isCreated()); enableMultiAssignments(); mvc.perform(post("/rest/v1/rollouts").content(invalideWeightRequest).contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.repo.constraintViolation"))); - mvc.perform(post("/rest/v1/rollouts").content(valideWeightRequest).contentType(MediaType.APPLICATION_JSON) + mvc.perform(post("/rest/v1/rollouts").content(valideWeightRequestMultiAssignment).contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) .andExpect(status().isCreated()); final List rollouts = rolloutManagement.findAll(PAGE, false).getContent(); - assertThat(rollouts).hasSize(1); + assertThat(rollouts).hasSize(2); assertThat(rollouts.get(0).getWeight()).get().isEqualTo(weight); } @@ -1542,7 +1543,7 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest { @Test @Description("Trigger next rollout group if rollout is in wrong state") void triggeringNextGroupRolloutWrongState() throws Exception { - final int amountTargets = 2; + final int amountTargets = 3; final List targets = testdataFactory.createTargets(amountTargets, "rollout"); final DistributionSet dsA = testdataFactory.createDistributionSet(""); diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java index 1226b1638..0d6d57d14 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java @@ -720,8 +720,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInt mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/{targetFilterQueryId}/autoAssignDS", filterId).content(valideWeightRequest).contentType(MediaType.APPLICATION_JSON)).andDo(print()) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.multiassignmentNotEnabled"))); + .andExpect(status().isOk()); enableMultiAssignments(); mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/{targetFilterQueryId}/autoAssignDS", filterId).content(invalideWeightRequest).contentType(MediaType.APPLICATION_JSON)).andDo(print()) diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java index efd59dfee..29ef464f4 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java @@ -2167,8 +2167,7 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(bodyValid.toString()) .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.multiassignmentNotEnabled"))); + .andExpect(status().isOk()); } @Test @@ -2197,28 +2196,17 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { final Long dsId = testdataFactory.createDistributionSet().getId(); final int customWeightHigh = 800; final int customWeightLow = 300; - assignDistributionSet(dsId, targetId); + assignDistributionSet(dsId, targetId); // default weight 1000 enableMultiAssignments(); assignDistributionSet(dsId, targetId, customWeightHigh); assignDistributionSet(dsId, targetId, customWeightLow); - // POSTGRESQL sets null values at the end, not the beginning - if (Database.POSTGRESQL.equals(jpaProperties.getDatabase())) { - mvc.perform(get("/rest/v1/targets/{targetId}/actions", targetId) - .param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "WEIGHT:ASC")) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) - .andExpect(jsonPath("content.[0].weight", equalTo(customWeightLow))) - .andExpect(jsonPath("content.[1].weight", equalTo(customWeightHigh))) - .andExpect(jsonPath("content.[2].weight").doesNotExist()); - } else { - mvc.perform(get("/rest/v1/targets/{targetId}/actions", targetId) - .param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "WEIGHT:ASC")) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) - .andExpect(jsonPath("content.[0].weight").doesNotExist()) - .andExpect(jsonPath("content.[1].weight", equalTo(customWeightLow))) - .andExpect(jsonPath("content.[2].weight", equalTo(customWeightHigh))); - } - + mvc.perform(get("/rest/v1/targets/{targetId}/actions", targetId) + .param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "WEIGHT:ASC")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("content.[0].weight", equalTo(customWeightLow))) + .andExpect(jsonPath("content.[1].weight", equalTo(customWeightHigh))) + .andExpect(jsonPath("content.[2].weight", equalTo(1000))); } @Test diff --git a/hawkbit-sdk/hawkbit-sdk-test/spring-shell.log b/hawkbit-sdk/hawkbit-sdk-test/spring-shell.log deleted file mode 100644 index f2707e9df..000000000 --- a/hawkbit-sdk/hawkbit-sdk-test/spring-shell.log +++ /dev/null @@ -1 +0,0 @@ -1703229421929:start diff --git a/pom.xml b/pom.xml index dafdc733a..7026d83c5 100644 --- a/pom.xml +++ b/pom.xml @@ -259,7 +259,7 @@ **/src/main/java/org/eclipse/hawkbit/ui/**,**/target/generated-sources/apt/**,**/src/main/java/org/eclipse/hawkbit/repository/test/**,**/examples/** - 0.8.8 + 0.8.11 ${project.build.directory} ${project.basedir}/../hawkbit-test-report/target/jacoco-aggregate/jacoco.xml,