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 jakarta.validation.constraints.NotNull;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; 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.RolloutCreate;
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
import org.eclipse.hawkbit.repository.builder.RolloutUpdate; import org.eclipse.hawkbit.repository.builder.RolloutUpdate;
@@ -78,6 +79,45 @@ public interface RolloutManagement {
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ) @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ)
long countByDistributionSetIdAndRolloutIsStoppable(long setId); 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 * Persists a new rollout entity. The filter within the
* {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets which * {@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 * 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 * targets have been assigned to the groups, the rollout status is changed to
* {@link RolloutStatus#READY} so it can be started with * {@link RolloutStatus#READY} so it can be started with
* {@link #start(Rollout)}. * {@link #start(long)}.
* *
* @param rollout * @param rollout
* the rollout entity to create * the rollout entity to create
@@ -274,8 +314,6 @@ public interface RolloutManagement {
* *
* @param rolloutId * @param rolloutId
* rollout id * rollout id
* @param deleted
* flag if deleted rollouts should be included
* @return rollout details of targets count for different statuses * @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}. * Builder for {@link Rollout}.
*
*/ */
@FunctionalInterface @FunctionalInterface
public interface RolloutGroupBuilder { public interface RolloutGroupBuilder {
@@ -22,5 +21,4 @@ public interface RolloutGroupBuilder {
* @return builder instance * @return builder instance
*/ */
RolloutGroupCreate create(); 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 * 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 * can be set at creation time. Other fields are set by the repository
* automatically, e.g. {@link BaseEntity#getCreatedAt()}. * automatically, e.g. {@link BaseEntity#getCreatedAt()}.
*
*/ */
public interface RolloutGroupCreate { public interface RolloutGroupCreate {
/** /**
* @param name * @param name for {@link Rollout#getName()}
* for {@link Rollout#getName()}
* @return updated builder instance * @return updated builder instance
*/ */
RolloutGroupCreate name(@Size(min = 1, max = NamedEntity.NAME_MAX_SIZE) @NotNull String name); RolloutGroupCreate name(@Size(min = 1, max = NamedEntity.NAME_MAX_SIZE) @NotNull String name);
/** /**
* @param description * @param description for {@link Rollout#getDescription()}
* for {@link Rollout#getDescription()}
* @return updated builder instance * @return updated builder instance
*/ */
RolloutGroupCreate description(@Size(max = NamedEntity.DESCRIPTION_MAX_SIZE) String description); RolloutGroupCreate description(@Size(max = NamedEntity.DESCRIPTION_MAX_SIZE) String description);
/** /**
* @param targetFilterQuery * @param targetFilterQuery for {@link Rollout#getTargetFilterQuery()}
* for {@link Rollout#getTargetFilterQuery()}
* @return updated builder instance * @return updated builder instance
*/ */
RolloutGroupCreate targetFilterQuery( RolloutGroupCreate targetFilterQuery(
@Size(min = 1, max = TargetFilterQuery.QUERY_MAX_SIZE) @NotNull String targetFilterQuery); @Size(min = 1, max = TargetFilterQuery.QUERY_MAX_SIZE) @NotNull String targetFilterQuery);
/** /**
* @param targetPercentage * @param targetPercentage the percentage of matching Targets that should be assigned to this Group
* the percentage of matching Targets that should be assigned to
* this Group
* @return updated builder instance * @return updated builder instance
*/ */
RolloutGroupCreate targetPercentage(Float targetPercentage); RolloutGroupCreate targetPercentage(Float targetPercentage);
/** /**
* @param conditions * @param conditions as created by {@link RolloutGroupConditionBuilder}.
* as created by {@link RolloutGroupConditionBuilder}.
* @return updated builder instance * @return updated builder instance
*/ */
RolloutGroupCreate conditions(RolloutGroupConditions conditions); RolloutGroupCreate conditions(RolloutGroupConditions conditions);
/** /**
* @param confirmationRequired * @param confirmationRequired if confirmation is required for this rollout group (considered
* if confirmation is required for this rollout group (considered
* with confirmation flow active) * with confirmation flow active)
* @return updated builder instance * @return updated builder instance
*/ */
@@ -76,5 +68,4 @@ public interface RolloutGroupCreate {
* @return peek on current state of {@link RolloutGroup} in the builder * @return peek on current state of {@link RolloutGroup} in the builder
*/ */
RolloutGroup build(); RolloutGroup build();
} }

View File

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

View File

@@ -78,7 +78,7 @@ public final class RolloutHelper {
*/ */
public static void verifyRolloutGroupParameter(final int amountGroup, final QuotaManagement quotaManagement) { public static void verifyRolloutGroupParameter(final int amountGroup, final QuotaManagement quotaManagement) {
if (amountGroup <= 0) { 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()) { } else if (amountGroup > quotaManagement.getMaxRolloutGroupsPerRollout()) {
throw new AssignmentQuotaExceededException( throw new AssignmentQuotaExceededException(
"The amount of groups cannot be greater than " + quotaManagement.getMaxRolloutGroupsPerRollout()); "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.RolloutGroupDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.RolloutStoppedEvent; import org.eclipse.hawkbit.repository.event.remote.RolloutStoppedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.RolloutUpdatedEvent; 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.exception.RolloutIllegalStateException;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.management.JpaRolloutManagement; 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 // When all groups are ready the rollout status can be changed to be
// ready, too. // ready, too.
if (readyGroups == rolloutGroups.size()) { 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 // 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); 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) { private void sendRolloutGroupDeletedEvents(final JpaRollout rollout) {
final List<Long> groupIds = rollout.getRolloutGroups().stream().map(RolloutGroup::getId) final List<Long> groupIds = rollout.getRolloutGroups().stream().map(RolloutGroup::getId).toList();
.collect(Collectors.toList());
afterCommit.afterCommit(() -> groupIds.forEach(rolloutGroupId -> eventPublisherHolder.getEventPublisher() afterCommit.afterCommit(() -> groupIds.forEach(rolloutGroupId -> eventPublisherHolder.getEventPublisher()
.publishEvent(new RolloutGroupDeletedEvent(tenantAware.getCurrentTenant(), rolloutGroupId, .publishEvent(new RolloutGroupDeletedEvent(tenantAware.getCurrentTenant(), rolloutGroupId,
JpaRolloutGroup.class, eventPublisherHolder.getApplicationId())))); JpaRolloutGroup.class, eventPublisherHolder.getApplicationId()))));
@@ -425,24 +424,11 @@ public class JpaRolloutExecutor implements RolloutExecutor {
executeRolloutGroupSuccessAction(rollout, latestRolloutGroup.get(0)); 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 // fakes getTotalTargets count to match expected for the last dynamic group
// so the evaluation to use total targets to properly // so the evaluation to use total targets to properly
private RolloutGroup evalProxy(final RolloutGroup group, final List<JpaRolloutGroup> rolloutGroups) { private RolloutGroup evalProxy(final RolloutGroup group, final List<JpaRolloutGroup> rolloutGroups) {
if (group.isDynamic()) { if (group.isDynamic()) {
final int expected = expectedDynamicGroupSize(rolloutGroups); final int expected = Math.max((int)group.getTargetPercentage(), 1);
return (RolloutGroup) Proxy.newProxyInstance( return (RolloutGroup) Proxy.newProxyInstance(
RolloutGroup.class.getClassLoader(), RolloutGroup.class.getClassLoader(),
new Class<?>[] {RolloutGroup.class}, new Class<?>[] {RolloutGroup.class},
@@ -702,8 +688,8 @@ public class JpaRolloutExecutor implements RolloutExecutor {
return false; return false;
} }
// expected as last full group - last static or previously filled in dynamic group // expected as last full group
final long expectedInGroup = expectedDynamicGroupSize(rolloutGroups); final long expectedInGroup = Math.max((int)group.getTargetPercentage(), 1);
final long currentlyInGroup = group.getTotalTargets(); final long currentlyInGroup = group.getTotalTargets();
if (currentlyInGroup >= expectedInGroup) { if (currentlyInGroup >= expectedInGroup) {
@@ -739,7 +725,7 @@ public class JpaRolloutExecutor implements RolloutExecutor {
updateTotalTargetCount(group, group.getTotalTargets() + newActions); updateTotalTargetCount(group, group.getTotalTargets() + newActions);
if (targetsLeftToAdd == 0) { 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); createDynamicGroup(rollout, group, rolloutGroups.size(), RolloutGroupStatus.SCHEDULED);
return true; 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) { 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 JpaRolloutGroup group = new JpaRolloutGroup();
final String nameAndDesc = "group-" + (groupCount + 1) + "-dynamic"; final String lastGroupWithoutSuffix = "group-" + groupCount;
group.setDynamic(true); final String suffix = lastGroup.getName().startsWith(lastGroupWithoutSuffix) ? lastGroup.getName().substring(lastGroupWithoutSuffix.length()) : "";
final String nameAndDesc = "group-" + (groupCount + 1) + suffix;
group.setName(nameAndDesc); group.setName(nameAndDesc);
group.setDescription(nameAndDesc); group.setDescription(nameAndDesc);
group.setRollout(rollout); group.setRollout(rollout);
group.setParent(lastGroup); 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) // 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) // or scheduled state (for next dynamic groups)
group.setStatus(status); group.setStatus(status);
group.setConfirmationRequired(lastGroup.isConfirmationRequired()); 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()); group.setTargetFilterQuery(lastGroup.getTargetFilterQuery());
addSuccessAndErrorConditionsAndActions(group, lastGroup.getSuccessCondition(), addSuccessAndErrorConditionsAndActions(group, lastGroup.getSuccessCondition(),

View File

@@ -22,8 +22,10 @@ import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import jakarta.validation.ConstraintDeclarationException; import jakarta.validation.ConstraintDeclarationException;
import jakarta.validation.Valid;
import jakarta.validation.ValidationException; import jakarta.validation.ValidationException;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.QuotaManagement; 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.TargetManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.ContextAware; 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.GenericRolloutUpdate;
import org.eclipse.hawkbit.repository.builder.RolloutCreate; import org.eclipse.hawkbit.repository.builder.RolloutCreate;
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
@@ -181,15 +184,37 @@ public class JpaRolloutManagement implements RolloutManagement {
return rolloutRepository.findById(rolloutId).map(Rollout.class::cast); 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 @Override
@Transactional @Transactional
@Retryable(include = { @Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) 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, public Rollout create(final RolloutCreate rollout, final int amountGroup, final boolean confirmationRequired,
final RolloutGroupConditions conditions) { 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); RolloutHelper.verifyRolloutGroupParameter(amountGroup, quotaManagement);
final JpaRollout savedRollout = createRollout((JpaRollout) rollout.build()); }
return createRolloutGroups(amountGroup, conditions, savedRollout, confirmationRequired);
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 @Override
@@ -199,13 +224,18 @@ public class JpaRolloutManagement implements RolloutManagement {
public Rollout create(final RolloutCreate rollout, final List<RolloutGroupCreate> groups, public Rollout create(final RolloutCreate rollout, final List<RolloutGroupCreate> groups,
final RolloutGroupConditions conditions) { final RolloutGroupConditions conditions) {
RolloutHelper.verifyRolloutGroupParameter(groups.size(), quotaManagement); 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); 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); WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(rollout);
final JpaDistributionSet distributionSet = (JpaDistributionSet) rollout.getDistributionSet(); final JpaDistributionSet distributionSet = (JpaDistributionSet) rollout.getDistributionSet();
if (pureDynamic) {
rollout.setTotalTargets(0);
} else {
final long totalTargets; final long totalTargets;
final String errMsg; final String errMsg;
if (RolloutHelper.isRolloutRetried(rollout.getTargetFilterQuery())) { if (RolloutHelper.isRolloutRetried(rollout.getTargetFilterQuery())) {
@@ -222,6 +252,7 @@ public class JpaRolloutManagement implements RolloutManagement {
throw new ValidationException(errMsg); throw new ValidationException(errMsg);
} }
rollout.setTotalTargets(totalTargets); rollout.setTotalTargets(totalTargets);
}
if (((JpaDistributionSetManagement)distributionSetManagement).isImplicitLockApplicable(distributionSet)) { if (((JpaDistributionSetManagement)distributionSetManagement).isImplicitLockApplicable(distributionSet)) {
distributionSetManagement.lock(distributionSet.getId()); distributionSetManagement.lock(distributionSet.getId());
@@ -235,23 +266,27 @@ public class JpaRolloutManagement implements RolloutManagement {
} }
private Rollout createRolloutGroups(final int amountOfGroups, final RolloutGroupConditions conditions, 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.verifyRolloutInStatus(rollout, RolloutStatus.CREATING);
RolloutHelper.verifyRolloutGroupConditions(conditions); RolloutHelper.verifyRolloutGroupConditions(conditions);
final JpaRollout savedRollout = rollout; RolloutGroup lastSavedGroup = null;
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 can enforce the 'max targets per group' quota right here because
// we want to distribute the targets equally to the different groups // we want to distribute the targets equally to the different groups
assertTargetsPerRolloutGroupQuota(rollout.getTotalTargets() / amountOfGroups); assertTargetsPerRolloutGroupQuota(rollout.getTotalTargets() / amountOfGroups);
RolloutGroup lastSavedGroup = null;
for (int i = 0; i < amountOfGroups; i++) { for (int i = 0; i < amountOfGroups; i++) {
final String nameAndDesc = "group-" + (i + 1); final String nameAndDesc = "group-" + (i + 1);
final JpaRolloutGroup group = new JpaRolloutGroup(); final JpaRolloutGroup group = new JpaRolloutGroup();
group.setName(nameAndDesc); group.setName(nameAndDesc);
group.setDescription(nameAndDesc); group.setDescription(nameAndDesc);
group.setRollout(savedRollout); group.setRollout(rollout);
group.setParent(lastSavedGroup); group.setParent(lastSavedGroup);
group.setStatus(RolloutGroupStatus.CREATING); group.setStatus(RolloutGroupStatus.CREATING);
group.setConfirmationRequired(isConfirmationRequired); group.setConfirmationRequired(isConfirmationRequired);
@@ -271,9 +306,31 @@ public class JpaRolloutManagement implements RolloutManagement {
lastSavedGroup = rolloutGroupRepository.save(group); lastSavedGroup = rolloutGroupRepository.save(group);
publishRolloutGroupCreatedEventAfterCommit(lastSavedGroup, rollout); publishRolloutGroupCreatedEventAfterCommit(lastSavedGroup, rollout);
} }
}
savedRollout.setRolloutGroupsCreated(amountOfGroups); if (dynamicRolloutGroupTemplate != null && rollout.isDynamic()) { // if not null then it is a dynamic rollout (already validated), but for sure
return rolloutRepository.save(savedRollout); // 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(rollout);
group.setParent(lastSavedGroup);
group.setDynamic(true);
group.setStatus(RolloutGroupStatus.READY);
group.setConfirmationRequired(isConfirmationRequired);
addSuccessAndErrorConditionsAndActions(group, conditions);
// for dynamic groups the target count is kept in target percentage
group.setTargetPercentage(dynamicRolloutGroupTemplate.getTargetCount());
lastSavedGroup = rolloutGroupRepository.save(group);
publishRolloutGroupCreatedEventAfterCommit(lastSavedGroup, rollout);
}
rollout.setRolloutGroupsCreated(lastSavedGroup.isDynamic() ? amountOfGroups + 1 : amountOfGroups);
return rolloutRepository.save(rollout);
} }
private Rollout createRolloutGroups(final List<RolloutGroupCreate> groupList, 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) { protected void assertRollout(final Rollout rollout, final boolean dynamic, final Rollout.RolloutStatus status, final int groupCreated, final long totalTargets) {
final Rollout refreshed = refresh(rollout); final Rollout refreshed = refresh(rollout);
assertThat(refreshed.isDynamic()).isEqualTo(dynamic); assertThat(refreshed.isDynamic()).as("Is dynamic").isEqualTo(dynamic);
assertThat(refreshed.getStatus()).isEqualTo(status); assertThat(refreshed.getStatus()).as("Status").isEqualTo(status);
assertThat(refreshed.getRolloutGroupsCreated()).isEqualTo(groupCreated); assertThat(refreshed.getRolloutGroupsCreated()).as("Groups created").isEqualTo(groupCreated);
assertThat(refreshed.getTotalTargets()).isEqualTo(totalTargets); assertThat(refreshed.getTotalTargets()).as("Total targets").isEqualTo(totalTargets);
} }
protected void assertGroup(final RolloutGroup group, final boolean dynamic, final RolloutGroup.RolloutGroupStatus status, final long totalTargets) { protected void assertGroup(final RolloutGroup group, final boolean dynamic, final RolloutGroup.RolloutGroupStatus status, final long totalTargets) {
final RolloutGroup refreshed = refresh(group); final RolloutGroup refreshed = refresh(group);
assertThat(refreshed.isDynamic()).isEqualTo(dynamic); assertThat(refreshed.isDynamic()).as("Is dynamic").isEqualTo(dynamic);
assertThat(refreshed.getStatus()).isEqualTo(status); assertThat(refreshed.getStatus()).as("Status").isEqualTo(status);
assertThat(refreshed.getTotalTargets()).isEqualTo(totalTargets); assertThat(refreshed.getTotalTargets()).as("Total targets").isEqualTo(totalTargets);
} }
protected Page<JpaAction> assertAndGetRunning(final Rollout rollout, final int count) { protected Page<JpaAction> assertAndGetRunning(final Rollout rollout, final int count) {
final Page<JpaAction> running = actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), Action.Status.RUNNING); 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; return running;
} }
protected void assertScheduled(final Rollout rollout, final int count) { protected void assertScheduled(final Rollout rollout, final int count) {
final Page<JpaAction> running = actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), Action.Status.SCHEDULED); 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) { 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.Feature;
import io.qameta.allure.Story; import io.qameta.allure.Story;
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; 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.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
@@ -85,7 +87,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest {
@Description("Verifies a simple dynamic rollout flow") @Description("Verifies a simple dynamic rollout flow")
void dynamicRolloutFlow() { void dynamicRolloutFlow() {
final String rolloutName = "dynamic-rollout-std"; 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 String targetPrefix = "controller-dynamic-rollout-std-";
final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName); 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 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( private void executeStaticWithoutOneTargetFromTheLastGroup(
final List<RolloutGroup> groups, final List<RolloutGroup> groups,
final Rollout rollout, final int amountGroups) { 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.TargetManagement;
import org.eclipse.hawkbit.repository.TargetTagManagement; import org.eclipse.hawkbit.repository.TargetTagManagement;
import org.eclipse.hawkbit.repository.TargetTypeManagement; 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.TagCreate;
import org.eclipse.hawkbit.repository.builder.TargetCreate; import org.eclipse.hawkbit.repository.builder.TargetCreate;
import org.eclipse.hawkbit.repository.builder.TargetTypeCreate; import org.eclipse.hawkbit.repository.builder.TargetTypeCreate;
@@ -1239,6 +1240,14 @@ public class TestdataFactory {
final int groupSize, final String filterQuery, final DistributionSet distributionSet, final int groupSize, final String filterQuery, final DistributionSet distributionSet,
final String successCondition, final String errorCondition, final Action.ActionType actionType, final String successCondition, final String errorCondition, final Action.ActionType actionType,
final Integer weight, final boolean confirmationRequired, final boolean dynamic) { 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() final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition) .successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition)
.errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition) .errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition)
@@ -1248,7 +1257,7 @@ public class TestdataFactory {
entityFactory.rollout().create().name(rolloutName).description(rolloutDescription) entityFactory.rollout().create().name(rolloutName).description(rolloutDescription)
.targetFilterQuery(filterQuery).distributionSetId(distributionSet).actionType(actionType).weight(weight) .targetFilterQuery(filterQuery).distributionSetId(distributionSet).actionType(actionType).weight(weight)
.dynamic(dynamic), .dynamic(dynamic),
groupSize, confirmationRequired, conditions); groupSize, confirmationRequired, conditions, dynamicRolloutGroupTemplate);
// Run here, because Scheduler is disabled during tests // Run here, because Scheduler is disabled during tests
rolloutHandler.handleAll(); rolloutHandler.handleAll();

View File

@@ -17,6 +17,7 @@ import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtDynamicRolloutGroupTemplate;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroup; import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroup;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@@ -87,6 +88,10 @@ public class MgmtRolloutRestRequestBodyPost extends AbstractMgmtRolloutCondition
@Schema(example = "true") @Schema(example = "true")
private boolean dynamic; private boolean dynamic;
@JsonProperty
@Schema(description = "Template for dynamic groups (only if dynamic flag is true)")
private MgmtDynamicRolloutGroupTemplate dynamicGroupTemplate;
@JsonProperty @JsonProperty
@Schema(description = """ @Schema(description = """
(Available with user consent flow active) If the confirmation is required for this rollout. Value will be used (Available with user consent flow active) If the confirmation is required for this rollout. Value will be used

View File

@@ -0,0 +1,33 @@
/**
* 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.mgmt.json.model.rolloutgroup;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* Model for defining the Attributes of a Rollout Group
*/
@Data
@Accessors(chain = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class MgmtDynamicRolloutGroupTemplate {
@Schema(description = "The name suffix of the dynamic groups", example = "-dynamic")
private String nameSuffix;
@Schema(description = "Count of targets a dynamic group shall include", example = "20")
private Long targetCount;
}

View File

@@ -14,6 +14,7 @@ import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.hawkbit.mgmt.json.model.rollout.AbstractMgmtRolloutConditionsEntity; import org.eclipse.hawkbit.mgmt.json.model.rollout.AbstractMgmtRolloutConditionsEntity;
@@ -26,12 +27,14 @@ import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPos
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPut; import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutSuccessAction; import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutSuccessAction;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutSuccessAction.SuccessAction; import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutSuccessAction.SuccessAction;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtDynamicRolloutGroupTemplate;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroup; import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroup;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroupResponseBody; import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroupResponseBody;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi;
import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.builder.DynamicRolloutGroupTemplate;
import org.eclipse.hawkbit.repository.builder.RolloutCreate; import org.eclipse.hawkbit.repository.builder.RolloutCreate;
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
import org.eclipse.hawkbit.repository.builder.RolloutUpdate; import org.eclipse.hawkbit.repository.builder.RolloutUpdate;
@@ -157,12 +160,21 @@ final class MgmtRolloutMapper {
} }
static RolloutGroupCreate fromRequest(final EntityFactory entityFactory, final MgmtRolloutGroup restRequest) { static RolloutGroupCreate fromRequest(final EntityFactory entityFactory, final MgmtRolloutGroup restRequest) {
return entityFactory.rolloutGroup().create().name(restRequest.getName()) return entityFactory.rolloutGroup().create().name(restRequest.getName())
.description(restRequest.getDescription()).targetFilterQuery(restRequest.getTargetFilterQuery()) .description(restRequest.getDescription()).targetFilterQuery(restRequest.getTargetFilterQuery())
.targetPercentage(restRequest.getTargetPercentage()).conditions(fromRequest(restRequest, false)); .targetPercentage(restRequest.getTargetPercentage()).conditions(fromRequest(restRequest, false));
} }
static DynamicRolloutGroupTemplate fromRequest(final MgmtDynamicRolloutGroupTemplate restRequest) {
if (restRequest == null) {
return null;
}
return DynamicRolloutGroupTemplate.builder()
.nameSuffix(Optional.ofNullable(restRequest.getNameSuffix()).orElse(""))
.targetCount(restRequest.getTargetCount())
.build();
}
static RolloutGroupConditions fromRequest(final AbstractMgmtRolloutConditionsEntity restRequest, static RolloutGroupConditions fromRequest(final AbstractMgmtRolloutConditionsEntity restRequest,
final boolean withDefaults) { final boolean withDefaults) {
final RolloutGroupConditionBuilder conditions = new RolloutGroupConditionBuilder(); final RolloutGroupConditionBuilder conditions = new RolloutGroupConditionBuilder();

View File

@@ -146,8 +146,14 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
final RolloutCreate create = MgmtRolloutMapper.fromRequest(entityFactory, rolloutRequestBody, distributionSet); final RolloutCreate create = MgmtRolloutMapper.fromRequest(entityFactory, rolloutRequestBody, distributionSet);
final boolean confirmationFlowActive = tenantConfigHelper.isConfirmationFlowEnabled(); final boolean confirmationFlowActive = tenantConfigHelper.isConfirmationFlowEnabled();
Rollout rollout; final Rollout rollout;
if (rolloutRequestBody.getGroups() != null) { if (rolloutRequestBody.getGroups() != null) {
if (rolloutRequestBody.isDynamic()) {
throw new ValidationException("Dynamic rollouts are not supported with groups");
}
if (rolloutRequestBody.getAmountGroups() != null) {
throw new ValidationException("Either 'amountGroups' or 'groups' must be defined in the request");
}
final List<RolloutGroupCreate> rolloutGroups = rolloutRequestBody.getGroups().stream() final List<RolloutGroupCreate> rolloutGroups = rolloutRequestBody.getGroups().stream()
.map(mgmtRolloutGroup -> { .map(mgmtRolloutGroup -> {
final boolean confirmationRequired = isConfirmationRequiredForGroup(mgmtRolloutGroup, final boolean confirmationRequired = isConfirmationRequiredForGroup(mgmtRolloutGroup,
@@ -156,14 +162,12 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
.confirmationRequired(confirmationRequired); .confirmationRequired(confirmationRequired);
}).collect(Collectors.toList()); }).collect(Collectors.toList());
rollout = rolloutManagement.create(create, rolloutGroups, rolloutGroupConditions); rollout = rolloutManagement.create(create, rolloutGroups, rolloutGroupConditions);
} else if (rolloutRequestBody.getAmountGroups() != null) { } else if (rolloutRequestBody.getAmountGroups() != null) {
final boolean confirmationRequired = rolloutRequestBody.getConfirmationRequired() == null final boolean confirmationRequired = rolloutRequestBody.getConfirmationRequired() == null
? confirmationFlowActive ? confirmationFlowActive
: rolloutRequestBody.getConfirmationRequired(); : rolloutRequestBody.getConfirmationRequired();
rollout = rolloutManagement.create(create, rolloutRequestBody.getAmountGroups(), confirmationRequired, rollout = rolloutManagement.create(create, rolloutRequestBody.getAmountGroups(), confirmationRequired,
rolloutGroupConditions); rolloutGroupConditions, MgmtRolloutMapper.fromRequest(rolloutRequestBody.getDynamicGroupTemplate()));
} else { } else {
throw new ValidationException("Either 'amountGroups' or 'groups' must be defined in the request"); throw new ValidationException("Either 'amountGroups' or 'groups' must be defined in the request");
} }
@@ -332,8 +336,7 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
final RolloutCreate create = MgmtRolloutMapper.fromRetriedRollout(entityFactory, rolloutForRetry); final RolloutCreate create = MgmtRolloutMapper.fromRetriedRollout(entityFactory, rolloutForRetry);
final RolloutGroupConditions groupConditions = new RolloutGroupConditionBuilder().withDefaults().build(); final RolloutGroupConditions groupConditions = new RolloutGroupConditionBuilder().withDefaults().build();
final Rollout retriedRollout = rolloutManagement.create(create, 1, false, final Rollout retriedRollout = rolloutManagement.create(create, 1, false, groupConditions, null);
groupConditions);
return ResponseEntity.status(HttpStatus.CREATED).body(MgmtRolloutMapper.toResponseRollout(retriedRollout, true)); return ResponseEntity.status(HttpStatus.CREATED).body(MgmtRolloutMapper.toResponseRollout(retriedRollout, true));
} }