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:
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user