From 2db45a4cc505dadefad4bd6dc57bea137c17704a Mon Sep 17 00:00:00 2001 From: Dimitar Shterev Date: Thu, 12 Jan 2023 14:22:09 +0200 Subject: [PATCH] =?UTF-8?q?Trigger=20next=20rollout=20group=20-=20backend?= =?UTF-8?q?=20and=20management=20API=20implementatio=E2=80=A6=20(#1294)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Trigger next rollout group - backend and management API implementations. Backend and management API tests. * Trigger next rollout group - Fixed resource documentation test. * Trigger next rollout group - Fixed resource documentation test. * add rest docs * Trigger next rollout group - UI changes. New button for trigger next rollout group in rollout view. * add error test for rest api * Trigger next rollout group - Added test for triggering next group for all rollout states. * add confirm * fix test * replace DB calls * fix translation * fix error message Signed-off-by: Dimitar Shterev Signed-off-by: Stefan Klotz Co-authored-by: Stefan Klotz --- .../hawkbit/repository/RolloutManagement.java | 16 ++++ .../repository/jpa/JpaRolloutManagement.java | 31 ++++++ .../jpa/RolloutGroupRepository.java | 1 - .../repository/jpa/RolloutManagementTest.java | 91 ++++++++++++++++++ .../mgmt/rest/api/MgmtRolloutRestApi.java | 12 +++ .../mgmt/rest/resource/MgmtRolloutMapper.java | 1 + .../rest/resource/MgmtRolloutResource.java | 6 ++ .../resource/MgmtRolloutResourceTest.java | 96 ++++++++++++++++++- .../src/main/asciidoc/rollout-api-guide.adoc | 33 +++++++ .../documentation/MgmtApiModelProperties.java | 1 + .../RolloutResourceDocumentationTest.java | 19 ++++ .../ui/rollout/rollout/RolloutGrid.java | 48 ++++++++++ .../ui/utils/UIComponentIdProvider.java | 10 ++ .../hawkbit/ui/utils/UIMessageIdProvider.java | 2 + .../src/main/resources/messages.properties | 8 ++ 15 files changed, 370 insertions(+), 5 deletions(-) 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 666cf3638..7dad3b438 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 @@ -463,4 +463,20 @@ public interface RolloutManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_UPDATE) void cancelRolloutsForDistributionSet(DistributionSet set); + /** + * Triggers next group of a rollout for processing even success threshold + * isn't met yet. Current running groups will not change their status. + * + * @param rolloutId + * the rollout to be paused. + * + * @throws EntityNotFoundException + * if rollout or group with given ID does not exist + * @throws RolloutIllegalStateException + * if given rollout is not in {@link RolloutStatus#RUNNING}. + * + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_UPDATE) + void triggerNextGroup(long rolloutId); + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java index d90fb19ec..1bba54f77 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java @@ -13,6 +13,7 @@ import static org.eclipse.hawkbit.repository.jpa.builder.JpaRolloutGroupCreate.a import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -48,6 +49,7 @@ import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecuto import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; import org.eclipse.hawkbit.repository.jpa.model.JpaRollout_; +import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRolloutGroupSuccessAction; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; import org.eclipse.hawkbit.repository.jpa.specifications.RolloutSpecification; import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper; @@ -128,6 +130,9 @@ public class JpaRolloutManagement implements RolloutManagement { @Autowired private RolloutStatusCache rolloutStatusCache; + @Autowired + private StartNextGroupRolloutGroupSuccessAction startNextRolloutGroupAction; + private final TargetManagement targetManagement; private final DistributionSetManagement distributionSetManagement; private final VirtualPropertyReplacer virtualPropertyReplacer; @@ -746,4 +751,30 @@ public class JpaRolloutManagement implements RolloutManagement { baseFilter, totalTargets, dsTypeId)); } + @Override + @Transactional + @Retryable(include = { + ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) + public void triggerNextGroup(final long rolloutId) { + final JpaRollout rollout = getRolloutAndThrowExceptionIfNotFound(rolloutId); + if (RolloutStatus.RUNNING != rollout.getStatus()) { + throw new RolloutIllegalStateException("Rollout is not in running state"); + } + final List groups = rollout.getRolloutGroups(); + + final boolean isNextGroupTriggerable = groups.stream() + .anyMatch(g -> RolloutGroupStatus.SCHEDULED.equals(g.getStatus())); + + if (!isNextGroupTriggerable) { + throw new RolloutIllegalStateException("Rollout does not have any groups left to be triggered"); + } + + final RolloutGroup latestRunning = groups.stream() + .sorted(Comparator.comparingLong(RolloutGroup::getId).reversed()) + .filter(g -> RolloutGroupStatus.RUNNING.equals(g.getStatus())).findFirst() + .orElseThrow(() -> new RolloutIllegalStateException("No group is running")); + + startNextRolloutGroupAction.eval(rollout, latestRunning, latestRunning.getSuccessActionExp()); + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutGroupRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutGroupRepository.java index ac08e3707..595fcfb22 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutGroupRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutGroupRepository.java @@ -173,5 +173,4 @@ public interface RolloutGroupRepository @Modifying @Query("DELETE FROM JpaRolloutGroup g where g.id in :rolloutGroupIds") void deleteByIds(@Param("rolloutGroupIds") List rolloutGroups); - } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java index 2ddf1fe21..a19380a6b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java @@ -213,6 +213,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { verifyThrownExceptionBy(() -> rolloutManagement.update(entityFactory.rollout().update(NOT_EXIST_IDL)), "Rollout"); + verifyThrownExceptionBy(() -> rolloutManagement.triggerNextGroup(NOT_EXIST_IDL), "Rollout"); } @Test @@ -2070,4 +2071,94 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { () -> rolloutManagement.get(myRolloutId).orElseThrow(NoSuchElementException::new)) .getStatus().equals(RolloutStatus.RUNNING)); } + + @Test + @Description("Verifying that next group is started on manual trigger next group.") + void checkRunningRolloutsManualTriggerNextGroup() { + + final int amountTargetsForRollout = 15; + final int amountOtherTargets = 0; + final int amountGroups = 3; + final String successCondition = "100"; + final String errorCondition = "80"; + + final Rollout createdRollout = createAndStartRollout(amountTargetsForRollout, amountOtherTargets, amountGroups, + successCondition, errorCondition); + + // triggers next group + rolloutManagement.triggerNextGroup(createdRollout.getId()); + + // second group should in running state + List rolloutGroups = rolloutGroupManagement + .findByRollout(new OffsetBasedPageRequest(0, 10, Sort.by(Direction.ASC, "id")), createdRollout.getId()) + .getContent(); + assertThat(rolloutGroups.get(0).getStatus()).isEqualTo(RolloutGroupStatus.RUNNING); + assertThat(rolloutGroups.get(1).getStatus()).isEqualTo(RolloutGroupStatus.RUNNING); + assertThat(rolloutGroups.get(2).getStatus()).isEqualTo(RolloutGroupStatus.SCHEDULED); + + // triggers next group + rolloutManagement.triggerNextGroup(createdRollout.getId()); + + // third group should be in running state + rolloutGroups = rolloutGroupManagement + .findByRollout(new OffsetBasedPageRequest(0, 10, Sort.by(Direction.ASC, "id")), createdRollout.getId()) + .getContent(); + assertThat(rolloutGroups.get(0).getStatus()).isEqualTo(RolloutGroupStatus.RUNNING); + assertThat(rolloutGroups.get(1).getStatus()).isEqualTo(RolloutGroupStatus.RUNNING); + assertThat(rolloutGroups.get(2).getStatus()).isEqualTo(RolloutGroupStatus.RUNNING); + + // finish action of all groups and verify rollout + final Slice runningActionsSlice = actionRepository.findByRolloutIdAndStatus(PAGE, + createdRollout.getId(), Status.RUNNING); + runningActionsSlice.getContent().forEach(this::finishAction); + + verifyRolloutAndAllGroupsAreFinished(createdRollout); + } + + @Test + @Description("Trigger next rollout group if rollout is in wrong state") + void triggeringNextGroupRolloutWrongState() { + + final int amountTargetsForRollout = 15; + final int amountOtherTargets = 0; + final int amountGroups = 3; + final String successCondition = "100"; + final String errorCondition = "80"; + + final String errorMessage = "Rollout is not in running state"; + + final Rollout createdRollout = createSimpleTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout, + amountOtherTargets, amountGroups, successCondition, errorCondition); + + // check CREATING state + assertThatExceptionOfType(RolloutIllegalStateException.class) + .isThrownBy(() -> rolloutManagement.triggerNextGroup(createdRollout.getId())) + .withMessageContaining(errorMessage); + + rolloutManagement.start(createdRollout.getId()); + // check STARTING state + assertThatExceptionOfType(RolloutIllegalStateException.class) + .isThrownBy(() -> rolloutManagement.triggerNextGroup(createdRollout.getId())) + .withMessageContaining(errorMessage); + + // Run here, because scheduler is disabled during tests + rolloutManagement.handleRollouts(); + final Rollout rollout = reloadRollout(createdRollout); + + rolloutManagement.pauseRollout(rollout.getId()); + + // check STOPPED state + assertThatExceptionOfType(RolloutIllegalStateException.class) + .isThrownBy(() -> rolloutManagement.triggerNextGroup(createdRollout.getId())) + .withMessageContaining(errorMessage); + + final Slice runningActionsSlice = actionRepository.findByRolloutIdAndStatus(PAGE, + createdRollout.getId(), Status.RUNNING); + runningActionsSlice.getContent().forEach(this::finishAction); + + // check FINISHED state + assertThatExceptionOfType(RolloutIllegalStateException.class) + .isThrownBy(() -> rolloutManagement.triggerNextGroup(createdRollout.getId())) + .withMessageContaining(errorMessage); + } } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java index a24b82654..55c7f777d 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java @@ -236,4 +236,16 @@ public interface MgmtRolloutRestApi { @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) int pagingLimitParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) String sortParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) String rsqlParam); + + /** + * Handles the POST request to force trigger processing next group of a rollout even success threshold isn't yet met + * + * @param rolloutId + * the ID of the rollout to trigger next group. + * @return OK response (200). In case of any + * exception the corresponding errors occur. + */ + @PostMapping(value = "/{rolloutId}/triggerNextGroup", produces = { MediaTypes.HAL_JSON_VALUE, + MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity triggerNextGroup(@PathVariable("rolloutId") Long rolloutId); } 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 8866404a4..a2bb9f458 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 @@ -95,6 +95,7 @@ final class MgmtRolloutMapper { body.add(linkTo(methodOn(MgmtRolloutRestApi.class).start(rollout.getId())).withRel("start")); body.add(linkTo(methodOn(MgmtRolloutRestApi.class).pause(rollout.getId())).withRel("pause")); body.add(linkTo(methodOn(MgmtRolloutRestApi.class).resume(rollout.getId())).withRel("resume")); + body.add(linkTo(methodOn(MgmtRolloutRestApi.class).triggerNextGroup(rollout.getId())).withRel("triggerNextGroup")); body.add(linkTo(methodOn(MgmtRolloutRestApi.class).approve(rollout.getId(), null)).withRel("approve")); body.add(linkTo(methodOn(MgmtRolloutRestApi.class).deny(rollout.getId(), null)).withRel("deny")); body.add(linkTo(methodOn(MgmtRolloutRestApi.class).getRolloutGroups(rollout.getId(), 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 3548ee085..12fabb1af 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 @@ -254,4 +254,10 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi { final List rest = MgmtTargetMapper.toResponse(rolloutGroupTargets.getContent()); return ResponseEntity.ok(new PagedList<>(rest, rolloutGroupTargets.getTotalElements())); } + + @Override + public ResponseEntity triggerNextGroup(@PathVariable("rolloutId") final Long rolloutId) { + this.rolloutManagement.triggerNextGroup(rolloutId); + return ResponseEntity.ok().build(); + } } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java index abf841134..61f888fa2 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.stream.Collectors; import org.awaitility.Awaitility; import org.awaitility.Duration; @@ -36,13 +37,16 @@ import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.RolloutGroup; +import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCondition; import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder; import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; +import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule; import org.eclipse.hawkbit.repository.test.util.WithUser; import org.eclipse.hawkbit.rest.util.JsonBuilder; @@ -52,6 +56,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort.Direction; import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultMatcher; import io.qameta.allure.Description; import io.qameta.allure.Feature; @@ -386,6 +391,8 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest { .andExpect(jsonPath("$._links.pause.href", allOf(startsWith(HREF_ROLLOUT_PREFIX), endsWith("/pause")))) .andExpect( jsonPath("$._links.resume.href", allOf(startsWith(HREF_ROLLOUT_PREFIX), endsWith("/resume")))) + .andExpect(jsonPath("$._links.triggerNextGroup.href", + allOf(startsWith(HREF_ROLLOUT_PREFIX), endsWith("/triggerNextGroup")))) .andExpect(jsonPath("$._links.groups.href", allOf(startsWith(HREF_ROLLOUT_PREFIX), containsString("/deploygroups")))) .andExpect(jsonPath("$.deleted", equalTo(false))); @@ -1033,14 +1040,95 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest { private Rollout createRollout(final String name, final int amountGroups, final long distributionSetId, final String targetFilterQuery) { - final Rollout rollout = rolloutManagement.create( - entityFactory.rollout().create().name(name).set(distributionSetId).targetFilterQuery(targetFilterQuery), - amountGroups, new RolloutGroupConditionBuilder().withDefaults() - .successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build()); + final Rollout rollout = createRolloutInCreatingSatate(name, amountGroups, distributionSetId, targetFilterQuery); // Run here, because Scheduler is disabled during tests rolloutManagement.handleRollouts(); return rolloutManagement.get(rollout.getId()).orElseThrow(NoSuchElementException::new); } + + private Rollout createRolloutInCreatingSatate(final String name, final int amountGroups, + final long distributionSetId, final String targetFilterQuery) { + return rolloutManagement.create( + entityFactory.rollout().create().name(name).set(distributionSetId).targetFilterQuery(targetFilterQuery), + amountGroups, new RolloutGroupConditionBuilder().withDefaults() + .successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build()); + } + + @Test + @Description("Trigger next rollout group") + void triggeringNextGroupRollout() throws Exception { + // setup + final int amountTargets = 20; + testdataFactory.createTargets(amountTargets, "rollout", "rollout"); + final DistributionSet dsA = testdataFactory.createDistributionSet(""); + + final Rollout rollout = createRollout("rollout1", 4, dsA.getId(), "controllerId==rollout*"); + rolloutManagement.start(rollout.getId()); + rolloutManagement.handleRollouts(); + + mvc.perform(post("/rest/v1/rollouts/{rolloutId}/triggerNextGroup", rollout.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + final List groupStatus = rolloutGroupManagement.findByRollout(PAGE, rollout.getId()) + .getContent().stream().map(RolloutGroup::getStatus).collect(Collectors.toList()); + assertThat(groupStatus).containsExactly(RolloutGroupStatus.RUNNING, RolloutGroupStatus.RUNNING, + RolloutGroupStatus.SCHEDULED, RolloutGroupStatus.SCHEDULED); + } + + @Test + @Description("Trigger next rollout group if rollout is in wrong state") + void triggeringNextGroupRolloutWrongState() throws Exception { + final int amountTargets = 2; + final List targets = testdataFactory.createTargets(amountTargets, "rollout"); + final DistributionSet dsA = testdataFactory.createDistributionSet(""); + + // CREATING state + final Rollout rollout = createRolloutInCreatingSatate("rollout1", 3, dsA.getId(), "controllerId==rollout*"); + triggerNextGroupAndExpect(rollout, status().isBadRequest()); + + // READY state + rolloutManagement.handleRollouts(); + triggerNextGroupAndExpect(rollout, status().isBadRequest()); + + // STARTING state + rolloutManagement.start(rollout.getId()); + triggerNextGroupAndExpect(rollout, status().isBadRequest()); + + // RUNNING state + rolloutManagement.handleRollouts(); + triggerNextGroupAndExpect(rollout, status().isOk()); + + // PAUSED state + rolloutManagement.pauseRollout(rollout.getId()); + triggerNextGroupAndExpect(rollout, status().isBadRequest()); + + rolloutManagement.resumeRollout(rollout.getId()); + triggerNextGroupAndExpect(rollout, status().isOk()); + + // last group already running + triggerNextGroupAndExpect(rollout, status().isBadRequest()); + + // FINISHED state + setTargetsStatus(targets, Status.FINISHED); + rolloutManagement.handleRollouts(); + triggerNextGroupAndExpect(rollout, status().isBadRequest()); + + } + + private void triggerNextGroupAndExpect(final Rollout rollout, final ResultMatcher expect) throws Exception { + mvc.perform(post("/rest/v1/rollouts/{rolloutId}/triggerNextGroup", rollout.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(expect); + } + + private void setTargetsStatus(final List targets, final Status status) { + for (final Target target : targets) { + final Long action = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).toList().get(0) + .getId(); + controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(action).status(status).message("test")); + } + } + } diff --git a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/rollout-api-guide.adoc b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/rollout-api-guide.adoc index 0ed88de6e..ad91778d7 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/rollout-api-guide.adoc +++ b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/rollout-api-guide.adoc @@ -356,6 +356,39 @@ include::../errors/406.adoc[] include::../errors/429.adoc[] |=== +== POST /rest/v1/rollouts/{rolloutId}/triggerNextGroup + +=== Implementation Notes +Handles the POST request of triggering the next group of a rollout within Hawkbit. Required Permission: UPDATE_ROLLOUT + +=== Trigger next group + + +==== CURL + +include::{snippets}/rollouts/trigger-next-group/curl-request.adoc[] + + +==== Request URL + +include::{snippets}/rollouts/trigger-next-group/http-request.adoc[] + +==== Response example + +include::{snippets}/rollouts/trigger-next-group/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/429.adoc[] +|=== + == DELETE /rest/v1/rollouts/{rolloutId} === Implementation Notes diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java index 32390657a..407a32efa 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java @@ -118,6 +118,7 @@ public final class MgmtApiModelProperties { public static final String ROLLOUT_LINKS_START_SYNC = "Link to start the rollout in sync mode"; public static final String ROLLOUT_LINKS_START_ASYNC = "Link to start the rollout in async mode"; public static final String ROLLOUT_LINKS_PAUSE = "Link to pause a running rollout"; + public static final String ROLLOUT_LINKS_TRIGGER_NEXT_GROUP = "Link for triggering next rollout group on a running rollout"; public static final String ROLLOUT_LINKS_RESUME = "Link to resume a paused rollout"; public static final String ROLLOUT_LINKS_APPROVE = "Link to approve a rollout"; public static final String ROLLOUT_LINKS_DENY = "Link to deny a rollout"; diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java index 631fe6c4e..0a7ca772b 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java @@ -147,6 +147,8 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati .description(MgmtApiModelProperties.ROLLOUT_LINKS_START_SYNC)); allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.pause") .description(MgmtApiModelProperties.ROLLOUT_LINKS_PAUSE)); + allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.triggerNextGroup") + .description(MgmtApiModelProperties.ROLLOUT_LINKS_TRIGGER_NEXT_GROUP)); allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.resume") .description(MgmtApiModelProperties.ROLLOUT_LINKS_RESUME)); allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.groups") @@ -444,6 +446,23 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati pathParameters(parameterWithName("rolloutId").description(ApiModelPropertiesGeneric.ITEM_ID)))); } + @Test + @Description("Handles the POST request of triggering the next group of a rollout. Required Permission: " + + SpPermission.UPDATE_ROLLOUT) + public void triggerNextGroup() throws Exception { + final Rollout rollout = createRolloutEntity(); + rolloutManagement.start(rollout.getId()); + + // Run here, because scheduler is disabled during tests + rolloutManagement.handleRollouts(); + + mockMvc.perform( + post(MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING + "/{rolloutId}/triggerNextGroup", rollout.getId()) + .accept(MediaTypes.HAL_JSON_VALUE)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andDo(this.document.document( + pathParameters(parameterWithName("rolloutId").description(ApiModelPropertiesGeneric.ITEM_ID)))); + } + @Test @Description("Handles the GET request of retrieving the deploy groups of a rollout. Required Permission: " + SpPermission.READ_ROLLOUT) diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutGrid.java index d0ce9eaf6..1bd1dd2f9 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutGrid.java @@ -19,12 +19,15 @@ import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; +import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus; import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus.Status; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.eclipse.hawkbit.ui.common.CommonUiDependencies; +import org.eclipse.hawkbit.ui.common.ConfirmationDialog; import org.eclipse.hawkbit.ui.common.builder.GridComponentBuilder; import org.eclipse.hawkbit.ui.common.builder.StatusIconBuilder.ActionTypeIconSupplier; import org.eclipse.hawkbit.ui.common.builder.StatusIconBuilder.RolloutStatusIconSupplier; @@ -54,6 +57,8 @@ import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.UIMessageIdProvider; import org.eclipse.hawkbit.ui.utils.UINotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.cronutils.utils.StringUtils; import com.google.common.base.Predicates; @@ -70,6 +75,8 @@ import com.vaadin.ui.renderers.HtmlRenderer; * Rollout list grid component. */ public class RolloutGrid extends AbstractGrid { + private static final Logger LOGGER = LoggerFactory.getLogger(RolloutGrid.class); + private static final long serialVersionUID = 1L; private static final String ROLLOUT_CAPTION_MSG_KEY = "caption.rollout"; @@ -88,6 +95,7 @@ public class RolloutGrid extends AbstractGrid { private static final String APPROVE_BUTTON_ID = "approve"; private static final String RUN_BUTTON_ID = "run"; private static final String PAUSE_BUTTON_ID = "pause"; + private static final String TRIGGER_NEXT_GROUP_BUTTON_ID = "triggerNextGroup"; private static final String UPDATE_BUTTON_ID = "update"; private static final String COPY_BUTTON_ID = "copy"; private static final String DELETE_BUTTON_ID = "delete"; @@ -226,6 +234,11 @@ public class RolloutGrid extends AbstractGrid { return RolloutStatus.RUNNING == status; } + private static boolean isTriggerNextGroupAllowed(final ProxyRollout rollout) { + final Long scheduled = rollout.getStatusTotalCountMap().get(TotalTargetCountStatus.Status.SCHEDULED); + return RolloutStatus.RUNNING == rollout.getStatus() && scheduled != null && scheduled > 0; + } + private static boolean isApprovingAllowed(final RolloutStatus status) { return RolloutStatus.WAITING_FOR_APPROVAL == status; } @@ -335,6 +348,15 @@ public class RolloutGrid extends AbstractGrid { permissionChecker.hasRolloutHandlePermission() && isPausingAllowed(rollout.getStatus())); actionColumns.add(GridComponentBuilder.addIconColumn(this, pauseButton, PAUSE_BUTTON_ID, null)); + final ValueProvider triggerNextGroupButton = rollout -> GridComponentBuilder + .buildActionButton(i18n, clickEvent -> triggerNextRolloutGroup(rollout.getId(), rollout.getStatus()), + VaadinIcons.STEP_FORWARD, UIMessageIdProvider.TOOLTIP_ROLLOUT_TRIGGER_NEXT_GROUP, + SPUIStyleDefinitions.STATUS_ICON_NEUTRAL, + UIComponentIdProvider.ROLLOUT_TRIGGER_NEXT_GROUP_BUTTON_ID + "." + rollout.getId(), + permissionChecker.hasRolloutUpdatePermission() && isTriggerNextGroupAllowed(rollout)); + actionColumns.add( + GridComponentBuilder.addIconColumn(this, triggerNextGroupButton, TRIGGER_NEXT_GROUP_BUTTON_ID, null)); + final ValueProvider updateButton = rollout -> GridComponentBuilder.buildActionButton(i18n, clickEvent -> updateRollout(rollout), VaadinIcons.EDIT, UIMessageIdProvider.TOOLTIP_ROLLOUT_UPDATE, SPUIStyleDefinitions.STATUS_ICON_NEUTRAL, @@ -488,4 +510,30 @@ public class RolloutGrid extends AbstractGrid { } return tooltipText.toString(); } + + private void triggerNextRolloutGroup(final Long rolloutId, final RolloutStatus rolloutStatus) { + if (RolloutStatus.RUNNING != rolloutStatus) { + uiNotification.displayValidationError(i18n.getMessage("message.rollout.trigger.next.group.not.running")); + } else { + final ConfirmationDialog triggerNextDialog = createTriggerNextGroupDialog(rolloutId); + UI.getCurrent().addWindow(triggerNextDialog.getWindow()); + triggerNextDialog.getWindow().bringToFront(); + } + } + + private ConfirmationDialog createTriggerNextGroupDialog(final Long rolloutId) { + final String caption = i18n.getMessage("caption.rollout.confirm.trigger.next"); + final String question = i18n.getMessage("message.rollout.confirm.trigger.next"); + return new ConfirmationDialog(i18n, caption, question, ok -> { + if (Boolean.TRUE.equals(ok)) { + try { + rolloutManagement.triggerNextGroup(rolloutId); + uiNotification.displaySuccess(i18n.getMessage("message.rollout.trigger.next.group.success")); + } catch (final RolloutIllegalStateException e) { + LOGGER.warn("Error on manually triggering next rollout group: {}", e.getMessage()); + uiNotification.displayValidationError(i18n.getMessage("message.rollout.trigger.next.group.error")); + } + } + }, UIComponentIdProvider.ROLLOUT_TRIGGER_NEXT_CONFIRMATION_DIALOG); + } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java index 9aaba4439..4564c1d4f 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java @@ -1154,6 +1154,11 @@ public final class UIComponentIdProvider { */ public static final String ROLLOUT_PAUSE_BUTTON_ID = ROLLOUT_ACTION_ID + ".7"; + /** + * Rollout trigger next group button id. + */ + public static final String ROLLOUT_TRIGGER_NEXT_GROUP_BUTTON_ID = ROLLOUT_ACTION_ID + ".13"; + /** * Rollout update button id. */ @@ -1465,6 +1470,11 @@ public final class UIComponentIdProvider { */ public static final String ROLLOUT_DELETE_CONFIRMATION_DIALOG = "rollout.delete.confirmation.window"; + /** + * Id of the rollout 'trigger next group' confirmation window + */ + public static final String ROLLOUT_TRIGGER_NEXT_CONFIRMATION_DIALOG = "rollout.triggernext.confirmation.window"; + /** * Id of the target filter deletion confirmation window */ diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java index 041fe199f..7144f2a90 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java @@ -351,6 +351,8 @@ public final class UIMessageIdProvider { public static final String TOOLTIP_UPLOAD_STATUS_PREFIX = "tooltip.upload.status."; + public static final String TOOLTIP_ROLLOUT_TRIGGER_NEXT_GROUP = "tooltip.rollout.triggernextgroup"; + /** * Private Constructor. */ diff --git a/hawkbit-ui/src/main/resources/messages.properties b/hawkbit-ui/src/main/resources/messages.properties index 6ded9db93..1dec8a264 100644 --- a/hawkbit-ui/src/main/resources/messages.properties +++ b/hawkbit-ui/src/main/resources/messages.properties @@ -336,6 +336,7 @@ tooltip.active.action.status.inactiveerror=In-active Error tooltip.rollout.run = Run tooltip.rollout.approve = Approve tooltip.rollout.pause = Pause +tooltip.rollout.triggernextgroup = Trigger next group tooltip.rollout.update = Edit.. tooltip.rollout.copy = Copy.. tooltip.delete = Delete.. @@ -773,6 +774,9 @@ message.rollout.max.group.size.exceeded.advanced = The maximum size of {0} targe message.rollout.approval.required = You should approve or reject the Rollout message.rollout.started = Rollout {0} started successfully message.rollout.paused = Rollout {0} paused successfully +message.rollout.trigger.next.group.not.running = Rollout is not in RUNNING state +message.rollout.trigger.next.group.success = Next group successfully triggered +message.rollout.trigger.next.group.error = Could not trigger next group message.rollout.resumed = Rollout {0} resumed successfully message.rollout.deleted = Rollout {0} deleted successfully message.rollout.noofgroups.or.targetfilter.missing = Please enter number of groups and select target filter @@ -808,6 +812,10 @@ caption.rollout.start.auto.desc = The rollout is started as soon as it is create caption.rollout.start.scheduled = Scheduled caption.rollout.start.scheduled.desc = The rollout starts as soon as it is ready and the set time has passed. label.rollout.calculating = Calculating groups ... + +caption.rollout.confirm.trigger.next = Trigger next rollout group +message.rollout.confirm.trigger.next = You are about to trigger the next rollout group.\nAre you sure? + #rollout - end #Menu