Add support for dynamic rollout group template (#1752)

1. Add support in REST and Mgmt API for dynamic group template
2. If present - groups follows the pattern of this template, otherwise - the last static group
3. This allows to create pure dynamic rollout with 0 static groups - auto assignment equivalent with groups

Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-06-26 08:31:01 +03:00
committed by GitHub
parent 40f99962d2
commit 8b3434fc17
15 changed files with 486 additions and 111 deletions

View File

@@ -18,6 +18,7 @@ import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.builder.DynamicRolloutGroupTemplate;
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
import org.eclipse.hawkbit.repository.builder.RolloutUpdate;
@@ -78,6 +79,45 @@ public interface RolloutManagement {
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ)
long countByDistributionSetIdAndRolloutIsStoppable(long setId);
/**
* Persists a new rollout entity. The filter within the
* {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets which
* are effected by this rollout to create. The amount of groups will be defined
* as equally sized.
*
* The rollout is not started. Only the preparation of the rollout is done,
* creating and persisting all the necessary groups. The Rollout and the groups
* are persisted in {@link RolloutStatus#CREATING} and
* {@link RolloutGroupStatus#CREATING}.
*
* The RolloutScheduler will start to assign targets to the groups. Once all
* targets have been assigned to the groups, the rollout status is changed to
* {@link RolloutStatus#READY} so it can be started with .
*
* @param create
* the rollout entity to create
* @param amountGroup
* the amount of groups to split the rollout into
* @param confirmationRequired
* if a confirmation is required by the device group(s) of the rollout
* @param conditions
* the rolloutgroup conditions and actions which should be applied
* for each {@link RolloutGroup}
* @param dynamicRolloutGroupTemplate the template for dynamic rollout groups
* @return the persisted rollout.
*
* @throws EntityNotFoundException
* if given {@link DistributionSet} does not exist
* @throws ConstraintViolationException
* if rollout or group parameters are invalid.
* @throws AssignmentQuotaExceededException
* if the maximum number of allowed targets per rollout group is
* exceeded.
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_CREATE)
Rollout create(@NotNull @Valid RolloutCreate create, int amountGroup, boolean confirmationRequired,
@NotNull RolloutGroupConditions conditions, DynamicRolloutGroupTemplate dynamicRolloutGroupTemplate);
/**
* Persists a new rollout entity. The filter within the
* {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets which
@@ -130,7 +170,7 @@ public interface RolloutManagement {
* The RolloutScheduler will start to assign targets to the groups. Once all
* targets have been assigned to the groups, the rollout status is changed to
* {@link RolloutStatus#READY} so it can be started with
* {@link #start(Rollout)}.
* {@link #start(long)}.
*
* @param rollout
* the rollout entity to create
@@ -274,8 +314,6 @@ public interface RolloutManagement {
*
* @param rolloutId
* rollout id
* @param deleted
* flag if deleted rollouts should be included
* @return rollout details of targets count for different statuses
*
*

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2024 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.builder;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
import org.eclipse.hawkbit.repository.model.RolloutGroupConditions;
/**
* Builder to create a new dynamic rollout group secret
*/
@Data
@Builder
public class DynamicRolloutGroupTemplate {
/**
* The name suffix, by default "" is used.
*/
@NotNull
private String nameSuffix = "";
/**
* The count of matching Targets that should be assigned to this Group
*/
private long targetCount;
/**
* The group conditions
*/
private RolloutGroupConditions conditions;
/**
* If confirmation is required for this rollout group (considered with confirmation flow active)
*/
private boolean confirmationRequired;
}

View File

@@ -13,7 +13,6 @@ import org.eclipse.hawkbit.repository.model.Rollout;
/**
* Builder for {@link Rollout}.
*
*/
@FunctionalInterface
public interface RolloutGroupBuilder {
@@ -22,5 +21,4 @@ public interface RolloutGroupBuilder {
* @return builder instance
*/
RolloutGroupCreate create();
}
}

View File

@@ -24,49 +24,41 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
* Builder to create a new {@link RolloutGroup} entry. Defines all fields that
* can be set at creation time. Other fields are set by the repository
* automatically, e.g. {@link BaseEntity#getCreatedAt()}.
*
*/
public interface RolloutGroupCreate {
/**
* @param name
* for {@link Rollout#getName()}
* @param name for {@link Rollout#getName()}
* @return updated builder instance
*/
RolloutGroupCreate name(@Size(min = 1, max = NamedEntity.NAME_MAX_SIZE) @NotNull String name);
/**
* @param description
* for {@link Rollout#getDescription()}
* @param description for {@link Rollout#getDescription()}
* @return updated builder instance
*/
RolloutGroupCreate description(@Size(max = NamedEntity.DESCRIPTION_MAX_SIZE) String description);
/**
* @param targetFilterQuery
* for {@link Rollout#getTargetFilterQuery()}
* @param targetFilterQuery for {@link Rollout#getTargetFilterQuery()}
* @return updated builder instance
*/
RolloutGroupCreate targetFilterQuery(
@Size(min = 1, max = TargetFilterQuery.QUERY_MAX_SIZE) @NotNull String targetFilterQuery);
/**
* @param targetPercentage
* the percentage of matching Targets that should be assigned to
* this Group
* @param targetPercentage the percentage of matching Targets that should be assigned to this Group
* @return updated builder instance
*/
RolloutGroupCreate targetPercentage(Float targetPercentage);
/**
* @param conditions
* as created by {@link RolloutGroupConditionBuilder}.
* @param conditions as created by {@link RolloutGroupConditionBuilder}.
* @return updated builder instance
*/
RolloutGroupCreate conditions(RolloutGroupConditions conditions);
/**
* @param confirmationRequired
* if confirmation is required for this rollout group (considered
* @param confirmationRequired if confirmation is required for this rollout group (considered
* with confirmation flow active)
* @return updated builder instance
*/
@@ -76,5 +68,4 @@ public interface RolloutGroupCreate {
* @return peek on current state of {@link RolloutGroup} in the builder
*/
RolloutGroup build();
}
}

View File

@@ -12,15 +12,12 @@ package org.eclipse.hawkbit.repository.builder;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.Rollout;
/**
* Builder to update an existing {@link Rollout} entry. Defines all fields that
* can be updated.
*
*/
public interface RolloutUpdate {
/**

View File

@@ -78,7 +78,7 @@ public final class RolloutHelper {
*/
public static void verifyRolloutGroupParameter(final int amountGroup, final QuotaManagement quotaManagement) {
if (amountGroup <= 0) {
throw new ValidationException("The amount of groups cannot be lower than zero");
throw new ValidationException("The amount of groups cannot be lower than or equal to zero for static rollouts");
} else if (amountGroup > quotaManagement.getMaxRolloutGroupsPerRollout()) {
throw new AssignmentQuotaExceededException(
"The amount of groups cannot be greater than " + quotaManagement.getMaxRolloutGroupsPerRollout());

View File

@@ -32,6 +32,7 @@ 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.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.management.JpaRolloutManagement;
@@ -221,7 +222,7 @@ 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()) {
if (rollout.isDynamic() && !rolloutGroups.get(rolloutGroups.size() - 1).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);
}
@@ -395,9 +396,7 @@ public class JpaRolloutExecutor implements RolloutExecutor {
}
private void sendRolloutGroupDeletedEvents(final JpaRollout rollout) {
final List<Long> groupIds = rollout.getRolloutGroups().stream().map(RolloutGroup::getId)
.collect(Collectors.toList());
final List<Long> groupIds = rollout.getRolloutGroups().stream().map(RolloutGroup::getId).toList();
afterCommit.afterCommit(() -> groupIds.forEach(rolloutGroupId -> eventPublisherHolder.getEventPublisher()
.publishEvent(new RolloutGroupDeletedEvent(tenantAware.getCurrentTenant(), rolloutGroupId,
JpaRolloutGroup.class, eventPublisherHolder.getApplicationId()))));
@@ -425,24 +424,11 @@ public class JpaRolloutExecutor implements RolloutExecutor {
executeRolloutGroupSuccessAction(rollout, latestRolloutGroup.get(0));
}
private static final int DEFAULT_DYNAMIC_GROUP_EXPECTED = 100;
private static int expectedDynamicGroupSize(final List<? extends RolloutGroup> 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<JpaRolloutGroup> rolloutGroups) {
if (group.isDynamic()) {
final int expected = expectedDynamicGroupSize(rolloutGroups);
final int expected = Math.max((int)group.getTargetPercentage(), 1);
return (RolloutGroup) Proxy.newProxyInstance(
RolloutGroup.class.getClassLoader(),
new Class<?>[] {RolloutGroup.class},
@@ -702,8 +688,8 @@ public class JpaRolloutExecutor implements RolloutExecutor {
return false;
}
// expected as last full group - last static or previously filled in dynamic group
final long expectedInGroup = expectedDynamicGroupSize(rolloutGroups);
// expected as last full group
final long expectedInGroup = Math.max((int)group.getTargetPercentage(), 1);
final long currentlyInGroup = group.getTotalTargets();
if (currentlyInGroup >= expectedInGroup) {
@@ -739,7 +725,7 @@ public class JpaRolloutExecutor implements RolloutExecutor {
updateTotalTargetCount(group, group.getTotalTargets() + newActions);
if (targetsLeftToAdd == 0) {
// this is filled create a new one in sheduled state
// this is filled create a new one in scheduled state
createDynamicGroup(rollout, group, rolloutGroups.size(), RolloutGroupStatus.SCHEDULED);
return true;
}
@@ -755,19 +741,32 @@ public class JpaRolloutExecutor implements RolloutExecutor {
}
private void createDynamicGroup(final JpaRollout rollout, final RolloutGroup lastGroup, final int groupCount, final RolloutGroupStatus status) {
try {
RolloutHelper.verifyRolloutGroupParameter(groupCount + 1, quotaManagement);
} catch (final AssignmentQuotaExceededException e) {
log.warn("Quota exceeded for dynamic rollout group creation: {}. Stop it", e.getMessage());
if (isRolloutComplete(rollout)) {
rollout.setStatus(RolloutStatus.STOPPED);
rolloutRepository.save(rollout);
}
return;
}
final JpaRolloutGroup group = new JpaRolloutGroup();
final String nameAndDesc = "group-" + (groupCount + 1) + "-dynamic";
group.setDynamic(true);
final String lastGroupWithoutSuffix = "group-" + groupCount;
final String suffix = lastGroup.getName().startsWith(lastGroupWithoutSuffix) ? lastGroup.getName().substring(lastGroupWithoutSuffix.length()) : "";
final String nameAndDesc = "group-" + (groupCount + 1) + suffix;
group.setName(nameAndDesc);
group.setDescription(nameAndDesc);
group.setRollout(rollout);
group.setParent(lastGroup);
group.setDynamic(true);
// 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());
// for dynamic groups the target count is kept in target percentage
group.setTargetPercentage(lastGroup.isDynamic() ? lastGroup.getTargetPercentage() : lastGroup.getTotalTargets());
group.setTargetFilterQuery(lastGroup.getTargetFilterQuery());
addSuccessAndErrorConditionsAndActions(group, lastGroup.getSuccessCondition(),

View File

@@ -22,8 +22,10 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import jakarta.validation.ConstraintDeclarationException;
import jakarta.validation.Valid;
import jakarta.validation.ValidationException;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.QuotaManagement;
@@ -36,6 +38,7 @@ import org.eclipse.hawkbit.repository.RolloutStatusCache;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.ContextAware;
import org.eclipse.hawkbit.repository.builder.DynamicRolloutGroupTemplate;
import org.eclipse.hawkbit.repository.builder.GenericRolloutUpdate;
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
@@ -181,15 +184,37 @@ public class JpaRolloutManagement implements RolloutManagement {
return rolloutRepository.findById(rolloutId).map(Rollout.class::cast);
}
@Override
@Transactional
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Rollout create(@NotNull @Valid RolloutCreate create, int amountGroup, boolean confirmationRequired,
@NotNull RolloutGroupConditions conditions) {
return create(create, amountGroup, confirmationRequired, conditions, null);
}
@Override
@Transactional
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Rollout create(final RolloutCreate rollout, final int amountGroup, final boolean confirmationRequired,
final RolloutGroupConditions conditions) {
RolloutHelper.verifyRolloutGroupParameter(amountGroup, quotaManagement);
final JpaRollout savedRollout = createRollout((JpaRollout) rollout.build());
return createRolloutGroups(amountGroup, conditions, savedRollout, confirmationRequired);
final RolloutGroupConditions conditions, final DynamicRolloutGroupTemplate dynamicRolloutGroupTemplate) {
if (amountGroup == 0) {
if (dynamicRolloutGroupTemplate == null) {
throw new ValidationException(
"When amount of groups is 0, the rollouts shall be dynamic and a dynamic group template must be provided");
}
} else {
RolloutHelper.verifyRolloutGroupParameter(amountGroup, quotaManagement);
}
final JpaRollout rolloutRequest = (JpaRollout) rollout.build();
if (dynamicRolloutGroupTemplate != null && !rolloutRequest.isDynamic()) {
throw new ValidationException("Dynamic group template is only allowed for dynamic rollouts");
}
final JpaRollout savedRollout = createRollout(rolloutRequest, amountGroup == 0);
return createRolloutGroups(amountGroup, conditions, savedRollout, confirmationRequired, dynamicRolloutGroupTemplate);
}
@Override
@@ -199,29 +224,35 @@ public class JpaRolloutManagement implements RolloutManagement {
public Rollout create(final RolloutCreate rollout, final List<RolloutGroupCreate> groups,
final RolloutGroupConditions conditions) {
RolloutHelper.verifyRolloutGroupParameter(groups.size(), quotaManagement);
final JpaRollout savedRollout = createRollout((JpaRollout) rollout.build());
final JpaRollout rolloutRequest = (JpaRollout) rollout.build();
final JpaRollout savedRollout = createRollout(rolloutRequest, false);
return createRolloutGroups(groups, conditions, savedRollout);
}
private JpaRollout createRollout(final JpaRollout rollout) {
private JpaRollout createRollout(final JpaRollout rollout, final boolean pureDynamic) {
WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(rollout);
final JpaDistributionSet distributionSet = (JpaDistributionSet) rollout.getDistributionSet();
final long totalTargets;
final String errMsg;
if (RolloutHelper.isRolloutRetried(rollout.getTargetFilterQuery())) {
totalTargets = targetManagement.countByFailedInRollout(
RolloutHelper.getIdFromRetriedTargetFilter(rollout.getTargetFilterQuery()),
distributionSet.getType().getId());
errMsg = "No failed targets in Rollout";
} else {
totalTargets = targetManagement.countByRsqlAndCompatible(rollout.getTargetFilterQuery(),
distributionSet.getType().getId());
errMsg = "Rollout does not match any existing targets";
if (pureDynamic) {
rollout.setTotalTargets(0);
} else {
final long totalTargets;
final String errMsg;
if (RolloutHelper.isRolloutRetried(rollout.getTargetFilterQuery())) {
totalTargets = targetManagement.countByFailedInRollout(
RolloutHelper.getIdFromRetriedTargetFilter(rollout.getTargetFilterQuery()),
distributionSet.getType().getId());
errMsg = "No failed targets in Rollout";
} else {
totalTargets = targetManagement.countByRsqlAndCompatible(rollout.getTargetFilterQuery(),
distributionSet.getType().getId());
errMsg = "Rollout does not match any existing targets";
}
if (totalTargets == 0) {
throw new ValidationException(errMsg);
}
rollout.setTotalTargets(totalTargets);
}
if (totalTargets == 0) {
throw new ValidationException(errMsg);
}
rollout.setTotalTargets(totalTargets);
if (((JpaDistributionSetManagement)distributionSetManagement).isImplicitLockApplicable(distributionSet)) {
distributionSetManagement.lock(distributionSet.getId());
@@ -235,45 +266,71 @@ public class JpaRolloutManagement implements RolloutManagement {
}
private Rollout createRolloutGroups(final int amountOfGroups, final RolloutGroupConditions conditions,
final JpaRollout rollout, final boolean isConfirmationRequired) {
final JpaRollout rollout, final boolean isConfirmationRequired, final DynamicRolloutGroupTemplate dynamicRolloutGroupTemplate) {
RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING);
RolloutHelper.verifyRolloutGroupConditions(conditions);
final JpaRollout savedRollout = rollout;
// we can enforce the 'max targets per group' quota right here because
// we want to distribute the targets equally to the different groups
assertTargetsPerRolloutGroupQuota(rollout.getTotalTargets() / amountOfGroups);
RolloutGroup lastSavedGroup = null;
for (int i = 0; i < amountOfGroups; i++) {
final String nameAndDesc = "group-" + (i + 1);
if (amountOfGroups == 0) {
if (dynamicRolloutGroupTemplate == null) {
throw new ConstraintDeclarationException(
"At least one static rollout group must be defined for a static rollout");
}
} else {
// we can enforce the 'max targets per group' quota right here because
// we want to distribute the targets equally to the different groups
assertTargetsPerRolloutGroupQuota(rollout.getTotalTargets() / amountOfGroups);
for (int i = 0; i < amountOfGroups; i++) {
final String nameAndDesc = "group-" + (i + 1);
final JpaRolloutGroup group = new JpaRolloutGroup();
group.setName(nameAndDesc);
group.setDescription(nameAndDesc);
group.setRollout(rollout);
group.setParent(lastSavedGroup);
group.setStatus(RolloutGroupStatus.CREATING);
group.setConfirmationRequired(isConfirmationRequired);
addSuccessAndErrorConditionsAndActions(group, conditions);
// 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);
}
}
if (dynamicRolloutGroupTemplate != null && rollout.isDynamic()) { // if not null then it is a dynamic rollout (already validated), but for sure
// create first template rollout group
final String nameAndDesc = "group-" + (amountOfGroups + 1) + dynamicRolloutGroupTemplate.getNameSuffix();
final JpaRolloutGroup group = new JpaRolloutGroup();
group.setName(nameAndDesc);
group.setDescription(nameAndDesc);
group.setRollout(savedRollout);
group.setRollout(rollout);
group.setParent(lastSavedGroup);
group.setStatus(RolloutGroupStatus.CREATING);
group.setDynamic(true);
group.setStatus(RolloutGroupStatus.READY);
group.setConfirmationRequired(isConfirmationRequired);
addSuccessAndErrorConditionsAndActions(group, conditions);
// 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);
// for dynamic groups the target count is kept in target percentage
group.setTargetPercentage(dynamicRolloutGroupTemplate.getTargetCount());
lastSavedGroup = rolloutGroupRepository.save(group);
publishRolloutGroupCreatedEventAfterCommit(lastSavedGroup, rollout);
}
savedRollout.setRolloutGroupsCreated(amountOfGroups);
return rolloutRepository.save(savedRollout);
rollout.setRolloutGroupsCreated(lastSavedGroup.isDynamic() ? amountOfGroups + 1 : amountOfGroups);
return rolloutRepository.save(rollout);
}
private Rollout createRolloutGroups(final List<RolloutGroupCreate> groupList,

View File

@@ -174,28 +174,28 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest
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);
assertThat(refreshed.isDynamic()).as("Is dynamic").isEqualTo(dynamic);
assertThat(refreshed.getStatus()).as("Status").isEqualTo(status);
assertThat(refreshed.getRolloutGroupsCreated()).as("Groups created").isEqualTo(groupCreated);
assertThat(refreshed.getTotalTargets()).as("Total targets").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);
assertThat(refreshed.isDynamic()).as("Is dynamic").isEqualTo(dynamic);
assertThat(refreshed.getStatus()).as("Status").isEqualTo(status);
assertThat(refreshed.getTotalTargets()).as("Total targets").isEqualTo(totalTargets);
}
protected Page<JpaAction> assertAndGetRunning(final Rollout rollout, final int count) {
final Page<JpaAction> running = actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), Action.Status.RUNNING);
assertThat(running.getTotalElements()).isEqualTo(count);
assertThat(running.getTotalElements()).as("Action count").isEqualTo(count);
return running;
}
protected void assertScheduled(final Rollout rollout, final int count) {
final Page<JpaAction> running = actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), Action.Status.SCHEDULED);
assertThat(running.getTotalElements()).isEqualTo(count);
assertThat(running.getTotalElements()).as("Action count").isEqualTo(count);
}
protected void finishAction(final Action action) {

View File

@@ -13,7 +13,9 @@ 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.builder.DynamicRolloutGroupTemplate;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
@@ -85,7 +87,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
@Description("Verifies a simple dynamic rollout flow")
void dynamicRolloutFlow() {
final String rolloutName = "dynamic-rollout-std";
final int amountGroups = 5; // static only
final int amountGroups = 3; // static only
final String targetPrefix = "controller-dynamic-rollout-std-";
final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName);
@@ -177,6 +179,193 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 2); // assign the target created when paused
}
@Test
@Description("Verifies a simple dynamic rollout flow with a dynamic group template")
void dynamicRolloutTemplateFlow() {
final String rolloutName = "dynamic-template-rollout-std";
final int amountGroups = 3; // static only
final String targetPrefix = "controller-template-dynamic-rollout-std-";
final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName);
// create rollout with amountGroups static groups * 3 targets and dynamic group template with 6 targets
testdataFactory.createTargets(targetPrefix, 0, amountGroups * 3);
final Rollout rollout = testdataFactory.createRolloutByVariables(rolloutName, rolloutName, amountGroups,
"controllerid==" + targetPrefix + "*", distributionSet, "60", "30",
Action.ActionType.FORCED, 1000, false, true,
DynamicRolloutGroupTemplate.builder().nameSuffix("-dyn").targetCount(6).build());
// rollout is READY, amountGroups + 1 (dynamic) rollout groups and amountGroups * 3 targets in static groups
assertRollout(rollout, true, RolloutStatus.READY, amountGroups + 1, amountGroups * 3);
List<RolloutGroup> 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 4 targets for the first dynamic group, fill partially
testdataFactory.createTargets(targetPrefix, amountGroups * 3, 4);
// 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);
executeStaticWithoutOneTargetFromTheLastGroup(groups, rollout, amountGroups);
// partially fill the first dynamic (it is running and now create actions for 4 targets)
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 1, amountGroups * 3 + 4);
assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 4);
// fill first (2) and create fill partially the second (+2 new)
testdataFactory.createTargets(targetPrefix, amountGroups * 3 + 4, 4);
rolloutHandler.handleAll(); // fill first dynamic group and create a new dynamic2
assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 6);
assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 6);
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 + 6);
assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 6);
assertGroup(dynamic2, true, RolloutGroupStatus.SCHEDULED, 0);
assertAndGetRunning(rollout, 7); // one from the last static group and 6 from the first dynamic
assertScheduled(rollout, 0);
// executes last from static and dynamic1 without 1 target
assertAndGetRunning(rollout, 7)// one from the last static and 6 from 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 + 5)
.forEach(this::finishAction);
assertAndGetRunning(rollout, 1); // remains on in the first dynamic
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 6);
assertGroup(groups.get(amountGroups - 1), false, RolloutGroupStatus.FINISHED, 3);
assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 6);
assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 0);
rolloutHandler.handleAll(); // add 2 action to now running second dynamic
assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 8);
assertAndGetRunning(rollout, 3);
assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 2);
testdataFactory.createTargets(targetPrefix, amountGroups * 3 + 8, 2);
rolloutManagement.pauseRollout(rollout.getId());
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.PAUSED, amountGroups + 2, amountGroups * 3 + 8);
assertAndGetRunning(rollout, 3);
assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 2); // no new assignment
rolloutManagement.resumeRollout(rollout.getId());
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 10);
assertAndGetRunning(rollout, 5);
assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 4); // assign the target created when paused
}
@Test
@Description("Verifies a simple pure (no static groups) dynamic rollout flow with a dynamic group template")
void dynamicRolloutPureFlow() {
final String rolloutName = "pure-dynamic-rollout-std";
final String targetPrefix = "controller-pure-dynamic-rollout-std-";
final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName);
final Rollout rollout = testdataFactory.createRolloutByVariables(rolloutName, rolloutName, 0,
"controllerid==" + targetPrefix + "*", distributionSet, "60", "30",
Action.ActionType.FORCED, 1000, false, true,
DynamicRolloutGroupTemplate.builder().nameSuffix("-dyn").targetCount(6).build());
// rollout is READY, amountGroups + 1 (dynamic) rollout groups and amountGroups * 3 targets in static groups
assertRollout(rollout, true, RolloutStatus.READY, 1, 0);
List<RolloutGroup> groups = rolloutGroupManagement.findByRollout(
new OffsetBasedPageRequest(0, 10, Sort.by(Direction.ASC, "id")),
rollout.getId()).getContent();
final RolloutGroup dynamic1 = groups.get(0);
assertRollout(rollout, true, RolloutStatus.READY, 1, 0); // + dynamic
assertGroup(dynamic1, true, RolloutGroupStatus.READY, 0);
// add 4 targets for the first dynamic group, fill partially
testdataFactory.createTargets(targetPrefix, 0, 4);
// start rollout
rolloutManagement.start(rollout.getId());
// handleStartingRollout (no handleRunning called yet)
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.RUNNING, 1, 0);
assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 0);
// partially fill the first dynamic (it is running and now create actions for 4 targets)
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.RUNNING, 1, 4);
assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 4);
// fill first (2) and create fill partially the second (+2 new)
testdataFactory.createTargets(targetPrefix, 4, 4);
rolloutHandler.handleAll(); // fill first dynamic group and create a new dynamic2
assertRollout(rollout, true, RolloutStatus.RUNNING, 2, 6);
assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 6);
groups = rolloutGroupManagement.findByRollout(
new OffsetBasedPageRequest(0, 10, Sort.by(Direction.ASC, "id")),
rollout.getId()).getContent();
final RolloutGroup dynamic2 = groups.get(1);
assertGroup(dynamic2, true, RolloutGroupStatus.SCHEDULED, 0);
// create scheduled actions for the dynamic2
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.RUNNING, 2, 6);
assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 6);
assertGroup(dynamic2, true, RolloutGroupStatus.SCHEDULED, 0);
assertAndGetRunning(rollout, 6); // 6 from the first dynamic
assertScheduled(rollout, 0);
// executes dynamic1 without 1 target
assertAndGetRunning(rollout, 6)// 6 from 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())) < 5)
.forEach(this::finishAction);
assertAndGetRunning(rollout, 1); // remains on in the first dynamic
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.RUNNING, 2, 6);
assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 6);
assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 0);
rolloutHandler.handleAll(); // add 2 action to now running second dynamic
assertRollout(rollout, true, RolloutStatus.RUNNING, 2, 8);
assertAndGetRunning(rollout, 3);
assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 2);
testdataFactory.createTargets(targetPrefix, 8, 2);
rolloutManagement.pauseRollout(rollout.getId());
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.PAUSED, 2, 8);
assertAndGetRunning(rollout, 3);
assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 2); // no new assignment
rolloutManagement.resumeRollout(rollout.getId());
rolloutHandler.handleAll();
assertRollout(rollout, true, RolloutStatus.RUNNING, 2, 10);
assertAndGetRunning(rollout, 5);
assertGroup(dynamic2, true, RolloutGroupStatus.RUNNING, 4); // assign the target created when paused
}
private void executeStaticWithoutOneTargetFromTheLastGroup(
final List<RolloutGroup> groups,
final Rollout rollout, final int amountGroups) {

View File

@@ -45,6 +45,7 @@ import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.TargetTagManagement;
import org.eclipse.hawkbit.repository.TargetTypeManagement;
import org.eclipse.hawkbit.repository.builder.DynamicRolloutGroupTemplate;
import org.eclipse.hawkbit.repository.builder.TagCreate;
import org.eclipse.hawkbit.repository.builder.TargetCreate;
import org.eclipse.hawkbit.repository.builder.TargetTypeCreate;
@@ -1239,6 +1240,14 @@ public class TestdataFactory {
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 boolean dynamic) {
return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet,
successCondition, errorCondition, actionType, weight, confirmationRequired, dynamic, null);
}
public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription,
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
final String successCondition, final String errorCondition, final Action.ActionType actionType,
final Integer weight, final boolean confirmationRequired, final boolean dynamic,
final DynamicRolloutGroupTemplate dynamicRolloutGroupTemplate) {
final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition)
.errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition)
@@ -1248,7 +1257,7 @@ public class TestdataFactory {
entityFactory.rollout().create().name(rolloutName).description(rolloutDescription)
.targetFilterQuery(filterQuery).distributionSetId(distributionSet).actionType(actionType).weight(weight)
.dynamic(dynamic),
groupSize, confirmationRequired, conditions);
groupSize, confirmationRequired, conditions, dynamicRolloutGroupTemplate);
// Run here, because Scheduler is disabled during tests
rolloutHandler.handleAll();