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,