From 8b3434fc177482338dae84c48f553c9f50d3eb95 Mon Sep 17 00:00:00 2001 From: Avgustin Marinov Date: Wed, 26 Jun 2024 08:31:01 +0300 Subject: [PATCH] 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 --- .../hawkbit/repository/RolloutManagement.java | 44 +++- .../builder/DynamicRolloutGroupTemplate.java | 44 ++++ .../builder/RolloutGroupBuilder.java | 4 +- .../builder/RolloutGroupCreate.java | 23 +-- .../repository/builder/RolloutUpdate.java | 3 - .../hawkbit/repository/RolloutHelper.java | 2 +- .../repository/jpa/JpaRolloutExecutor.java | 47 +++-- .../jpa/management/JpaRolloutManagement.java | 143 +++++++++---- .../jpa/AbstractJpaIntegrationTest.java | 18 +- .../management/RolloutManagementFlowTest.java | 191 +++++++++++++++++- .../repository/test/util/TestdataFactory.java | 11 +- .../MgmtRolloutRestRequestBodyPost.java | 5 + .../MgmtDynamicRolloutGroupTemplate.java | 33 +++ .../mgmt/rest/resource/MgmtRolloutMapper.java | 14 +- .../rest/resource/MgmtRolloutResource.java | 15 +- 15 files changed, 486 insertions(+), 111 deletions(-) create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/DynamicRolloutGroupTemplate.java create mode 100644 hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rolloutgroup/MgmtDynamicRolloutGroupTemplate.java diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java index 9f6258e2d..1b45771e2 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java @@ -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 * * diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/DynamicRolloutGroupTemplate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/DynamicRolloutGroupTemplate.java new file mode 100644 index 000000000..1ce056267 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/DynamicRolloutGroupTemplate.java @@ -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; +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutGroupBuilder.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutGroupBuilder.java index 632d74d66..90f62f746 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutGroupBuilder.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutGroupBuilder.java @@ -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(); - -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutGroupCreate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutGroupCreate.java index 53b3f0ea2..0558ecc0f 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutGroupCreate.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutGroupCreate.java @@ -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(); - -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutUpdate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutUpdate.java index ff5ebb535..290d8cc04 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutUpdate.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutUpdate.java @@ -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 { /** diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutHelper.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutHelper.java index 13e4db84f..5c76be5c8 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutHelper.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutHelper.java @@ -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()); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java index bd7b05db9..c6f70ec99 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java @@ -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 groupIds = rollout.getRolloutGroups().stream().map(RolloutGroup::getId) - .collect(Collectors.toList()); - + final List 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 rolloutGroups) { - int expected = 0; - // gets the size of last static group (it is a pattern for dynamic groups) - for (final RolloutGroup rolloutGroup : rolloutGroups) { - if (rolloutGroup.isDynamic()) { - break; - } - expected = rolloutGroup.getTotalTargets(); - } - return expected <= 0 ? DEFAULT_DYNAMIC_GROUP_EXPECTED /* default if the last static group has been empty */ : expected; - } - // fakes getTotalTargets count to match expected for the last dynamic group // so the evaluation to use total targets to properly private RolloutGroup evalProxy(final RolloutGroup group, final List rolloutGroups) { if (group.isDynamic()) { - final int expected = expectedDynamicGroupSize(rolloutGroups); + 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(), diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java index e95839ba9..f4bcdeb46 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java @@ -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 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 groupList, diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java index 0a347712c..998e963b7 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java @@ -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 assertAndGetRunning(final Rollout rollout, final int count) { final Page 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 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) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementFlowTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementFlowTest.java index 8f63124c8..8b6dad91c 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementFlowTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementFlowTest.java @@ -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 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 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 groups, final Rollout rollout, final int amountGroups) { diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java index e747bc906..9388166b8 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java @@ -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(); diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBodyPost.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBodyPost.java index d9f06bed8..f5e962b00 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBodyPost.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBodyPost.java @@ -17,6 +17,7 @@ import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.experimental.Accessors; 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 com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -87,6 +88,10 @@ public class MgmtRolloutRestRequestBodyPost extends AbstractMgmtRolloutCondition @Schema(example = "true") private boolean dynamic; + @JsonProperty + @Schema(description = "Template for dynamic groups (only if dynamic flag is true)") + private MgmtDynamicRolloutGroupTemplate dynamicGroupTemplate; + @JsonProperty @Schema(description = """ (Available with user consent flow active) If the confirmation is required for this rollout. Value will be used diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rolloutgroup/MgmtDynamicRolloutGroupTemplate.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rolloutgroup/MgmtDynamicRolloutGroupTemplate.java new file mode 100644 index 000000000..da4b237ca --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rolloutgroup/MgmtDynamicRolloutGroupTemplate.java @@ -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; +} \ No newline at end of file diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java index 5e9ecdaaa..014e1060e 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java @@ -14,6 +14,7 @@ import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; 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.MgmtRolloutSuccessAction; 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.MgmtRolloutGroupResponseBody; import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi; 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.RolloutGroupCreate; import org.eclipse.hawkbit.repository.builder.RolloutUpdate; @@ -157,12 +160,21 @@ final class MgmtRolloutMapper { } static RolloutGroupCreate fromRequest(final EntityFactory entityFactory, final MgmtRolloutGroup restRequest) { - return entityFactory.rolloutGroup().create().name(restRequest.getName()) .description(restRequest.getDescription()).targetFilterQuery(restRequest.getTargetFilterQuery()) .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, final boolean withDefaults) { final RolloutGroupConditionBuilder conditions = new RolloutGroupConditionBuilder(); diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java index cb8c47fa4..16c2b4dff 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java @@ -146,8 +146,14 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi { final RolloutCreate create = MgmtRolloutMapper.fromRequest(entityFactory, rolloutRequestBody, distributionSet); final boolean confirmationFlowActive = tenantConfigHelper.isConfirmationFlowEnabled(); - Rollout rollout; + final Rollout rollout; 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 rolloutGroups = rolloutRequestBody.getGroups().stream() .map(mgmtRolloutGroup -> { final boolean confirmationRequired = isConfirmationRequiredForGroup(mgmtRolloutGroup, @@ -156,14 +162,12 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi { .confirmationRequired(confirmationRequired); }).collect(Collectors.toList()); rollout = rolloutManagement.create(create, rolloutGroups, rolloutGroupConditions); - } else if (rolloutRequestBody.getAmountGroups() != null) { final boolean confirmationRequired = rolloutRequestBody.getConfirmationRequired() == null ? confirmationFlowActive : rolloutRequestBody.getConfirmationRequired(); rollout = rolloutManagement.create(create, rolloutRequestBody.getAmountGroups(), confirmationRequired, - rolloutGroupConditions); - + rolloutGroupConditions, MgmtRolloutMapper.fromRequest(rolloutRequestBody.getDynamicGroupTemplate())); } else { 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 RolloutGroupConditions groupConditions = new RolloutGroupConditionBuilder().withDefaults().build(); - final Rollout retriedRollout = rolloutManagement.create(create, 1, false, - groupConditions); + final Rollout retriedRollout = rolloutManagement.create(create, 1, false, groupConditions, null); return ResponseEntity.status(HttpStatus.CREATED).body(MgmtRolloutMapper.toResponseRollout(retriedRollout, true)); }