diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtInvalidateDistributionSetRequestBody.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtInvalidateDistributionSetRequestBody.java index b5721e978..898c0f2b0 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtInvalidateDistributionSetRequestBody.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtInvalidateDistributionSetRequestBody.java @@ -25,7 +25,5 @@ public class MgmtInvalidateDistributionSetRequestBody { @NotNull @Schema(description = "Type of cancelation for actions referring to the given distribution set") private MgmtCancelationType actionCancelationType; - - @Schema(description = "Defines if rollouts referring to this distribution set should be canceled", example = "true") - private boolean cancelRollouts; + } \ No newline at end of file diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java index 12ad8df80..634b236ca 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java @@ -348,6 +348,39 @@ public interface MgmtRolloutRestApi { produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) ResponseEntity pause(@PathVariable("rolloutId") Long rolloutId); + + /** + * Handles the POST request for stopping a rollout. + * + * @param rolloutId the ID of the rollout to be paused. + * @return OK response (200) if rollout could be stopped. In case of any exception the corresponding errors occur. + */ + @Operation(summary = "Stop a Rollout", description = "Handles the POST request of stopping a running rollout. " + + "Required Permission: HANDLE_ROLLOUT") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))), + @ApiResponse(responseCode = "401", description = "The request requires user authentication.", + content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions, entity is not allowed to be changed (i.e. read-only) or " + + "data volume restriction applies.", + content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", description = "Rollout not found.", + content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.", + content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.", + content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " + + "and the client has to wait another second.", + content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))) + }) + @PostMapping(value = MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING + "/{rolloutId}/stop", + produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity stop(@PathVariable("rolloutId") Long rolloutId); + /** * Handles the DELETE request for deleting a rollout. * diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java index b1c2e51be..bfab8a7b9 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java @@ -360,8 +360,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { distributionSetInvalidationManagement .invalidateDistributionSet( new DistributionSetInvalidation(Collections.singletonList(distributionSetId), - MgmtRestModelMapper.convertCancelationType(invalidateRequestBody.getActionCancelationType()), - invalidateRequestBody.isCancelRollouts())); + MgmtRestModelMapper.convertCancelationType(invalidateRequestBody.getActionCancelationType()))); return ResponseEntity.ok().build(); } } \ No newline at end of file diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java index ad25d8803..fe7329efe 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java @@ -185,6 +185,13 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi { return ResponseEntity.ok().build(); } + @Override + @AuditLog(entity = "Rollout", type = AuditLog.Type.UPDATE, description = "Stop Rollout") + public ResponseEntity stop(Long rolloutId) { + this.rolloutManagement.stop(rolloutId); + return ResponseEntity.ok().build(); + } + @Override @AuditLog(entity = "Rollout", type = AuditLog.Type.DELETE, description = "Delete Rollout") public ResponseEntity delete(final Long rolloutId) { diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtRestModelMapper.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtRestModelMapper.java index df3392d53..f60bd03e8 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtRestModelMapper.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtRestModelMapper.java @@ -17,7 +17,7 @@ import org.eclipse.hawkbit.mgmt.json.model.MgmtTypeEntity; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtCancelationType; import org.eclipse.hawkbit.repository.model.Action.ActionType; -import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.NamedEntity; import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity; import org.eclipse.hawkbit.repository.model.Type; @@ -67,20 +67,20 @@ public final class MgmtRestModelMapper { } /** - * Converts the given repository {@link CancelationType} into a corresponding {@link MgmtCancelationType}. + * Converts the given repository {@link ActionCancellationType} into a corresponding {@link MgmtCancelationType}. * * @param cancelationType the repository representation of the cancellation type * @return or the REST cancellation type */ - public static CancelationType convertCancelationType(final MgmtCancelationType cancelationType) { + public static ActionCancellationType convertCancelationType(final MgmtCancelationType cancelationType) { if (cancelationType == null) { return null; } return switch (cancelationType) { - case SOFT -> CancelationType.SOFT; - case FORCE -> CancelationType.FORCE; - case NONE -> CancelationType.NONE; + case SOFT -> ActionCancellationType.SOFT; + case FORCE -> ActionCancellationType.FORCE; + case NONE -> ActionCancellationType.NONE; }; } diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java index 1f9d88369..acb90b245 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java @@ -1685,7 +1685,7 @@ class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegrationTe * Verify invalidation of distribution sets that removes distribution sets from auto assignments, stops rollouts and cancels assignments */ @Test - void invalidateDistributionSet() throws Exception { + void softInvalidateDistributionSet() throws Exception { DistributionSet distributionSet = testdataFactory.createDistributionSet(); final List targets = testdataFactory.createTargets(5, "invalidateDistributionSet"); // the distribution set is locked and the old instance become stale @@ -1697,7 +1697,6 @@ class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegrationTe final JSONObject jsonObject = new JSONObject(); jsonObject.put("actionCancelationType", "soft"); - jsonObject.put("cancelRollouts", true); mvc.perform(post("/rest/v1/distributionsets/{ds}/invalidate", distributionSet.getId()) .content(jsonObject.toString()).contentType(MediaType.APPLICATION_JSON)) @@ -1706,7 +1705,11 @@ class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegrationTe assertThat(targetFilterQueryManagement.get(targetFilterQuery.getId()).get().getAutoAssignDistributionSet()) .isNull(); assertThat(rolloutManagement.get(rollout.getId()).get().getStatus()).isIn(RolloutStatus.STOPPING, - RolloutStatus.FINISHED); + RolloutStatus.STOPPED); + //then enforce executor to stop the rollout and check + rolloutHandler.handleAll(); + assertThat(rolloutManagement.get(rollout.getId()).get().getStatus()).isIn(RolloutStatus.STOPPED); + for (final Target target : targets) { assertThat(targetManagement.get(target.getId()).get().getUpdateStatus()) .isEqualTo(TargetUpdateStatus.PENDING); @@ -1717,6 +1720,69 @@ class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegrationTe } } + @Test + void forceInvalidateDistributionSet() throws Exception { + DistributionSet distributionSet = testdataFactory.createDistributionSet(); + final List targets = testdataFactory.createTargets(5, "invalidateDistributionSet"); + distributionSet = assignDistributionSet(distributionSet, targets).getDistributionSet(); + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create( + Create.builder().name("invalidateDistributionSet").query("name==*").autoAssignDistributionSet(distributionSet).build()); + final Rollout rollout = testdataFactory.createRolloutByVariables("invalidateDistributionSet", "desc", 2, + "name==*", distributionSet, "50", "80"); + + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("actionCancelationType", "force"); + + mvc.perform(post("/rest/v1/distributionsets/{ds}/invalidate", distributionSet.getId()) + .content(jsonObject.toString()).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + assertThat(targetFilterQueryManagement.get(targetFilterQuery.getId()).get().getAutoAssignDistributionSet()) + .isNull(); + assertThat(rolloutManagement.get(rollout.getId()).get().getStatus()).isIn(RolloutStatus.DELETING, + RolloutStatus.DELETED); + //then enforce executor to stop the rollout and check + rolloutHandler.handleAll(); + // assert rollout is deleted + assertThat(rolloutManagement.get(rollout.getId())).isEmpty(); + + for (final Target target : targets) { + assertThat(targetManagement.get(target.getId()).get().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.IN_SYNC); + assertThat(deploymentManagement.findActionsByTarget(target.getControllerId(), PageRequest.of(0, 100)) + .getNumberOfElements()).isEqualTo(1); + assertThat(deploymentManagement.findActionsByTarget(target.getControllerId(), PageRequest.of(0, 100)) + .getContent().get(0).getStatus()).isEqualTo(Status.CANCELED); + } + } + + @Test + void invalidateDistributionSetWithNoneCancellation() throws Exception { + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); + final List targets = testdataFactory.createTargets(5, "invalidateDistributionSet"); + Rollout rollout = testdataFactory.createRolloutByVariables("invalidateDistributionSet", "desc", 1, + "name==*", distributionSet, "50", "80"); + rollout = testdataFactory.startRollout(rollout); + + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("actionCancelationType", "none"); + + mvc.perform(post("/rest/v1/distributionsets/{ds}/invalidate", distributionSet.getId()) + .content(jsonObject.toString()).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + assertThat(rolloutManagement.get(rollout.getId()).get().getStatus()).isIn(RolloutStatus.RUNNING); + + for (final Target target : targets) { + assertThat(targetManagement.get(target.getId()).get().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + assertThat(deploymentManagement.findActionsByTarget(target.getControllerId(), PageRequest.of(0, 100)) + .getNumberOfElements()).isEqualTo(1); + assertThat(deploymentManagement.findActionsByTarget(target.getControllerId(), PageRequest.of(0, 100)) + .getContent().get(0).getStatus()).isEqualTo(Status.RUNNING); + } + } + /** * Tests the lock. It is verified that the distribution set can be marked as locked through update operation. */ diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java index 79fabec08..0455de333 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java @@ -31,6 +31,7 @@ import java.util.Arrays; import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import org.awaitility.Awaitility; @@ -1461,6 +1462,55 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest { .andExpect(jsonPath("$.deleted", equalTo(true))); assertStatusIs(rollout, RolloutStatus.DELETED); + + List rolloutActions = + deploymentManagement.findActions("rollout.id==" + rollout.getId(), PAGE).getContent(); + for (Action action : rolloutActions) { + Assertions.assertEquals(Status.CANCELED, action.getStatus()); + } + + // ensure groups are in final state + List groups = rolloutGroupManagement.findByRollout(rollout.getId(), PAGE).getContent(); + for (RolloutGroup rolloutGroup : groups) { + Assertions.assertEquals(RolloutGroupStatus.FINISHED, rolloutGroup.getStatus()); + } + } + + @Test + void stopRunningRollout() throws Exception { + final Rollout rollout = testdataFactory.createAndStartRollout(); + mvc.perform(post("/rest/v1/rollouts/{rolloutId}/stop", rollout.getId())) + .andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + mvc.perform(get("/rest/v1/rollouts/{rolloutid}", rollout.getId())) + .andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status", equalTo("stopping"))); + + // force executor to retrigger + rolloutHandler.handleAll(); + + List rolloutActions = + deploymentManagement.findActions("rollout.id==" + rollout.getId(), PAGE).getContent(); + for (Action action : rolloutActions) { + Awaitility.await() + .atMost(30, TimeUnit.SECONDS) + .untilAsserted(() -> Assertions.assertEquals(Status.CANCELING, action.getStatus())); + } + + // assume that the targets have agreed to cancel the actions + rolloutActions.forEach(action -> controllerManagement.addCancelActionStatus( + Action.ActionStatusCreate.builder().actionId(action.getId()).status(Status.CANCELED).build() + )); + + // force executor to retrigger + rolloutHandler.handleAll(); + // rollout should be in stopped state after all actions are cancelled + mvc.perform(get("/rest/v1/rollouts/{rolloutid}", rollout.getId())) + .andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status", equalTo("stopped"))); } /** diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java index 42c60fa8b..8ca2f9ca4 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java @@ -31,12 +31,12 @@ import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; -import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.Target; @@ -421,5 +421,5 @@ public interface DeploymentManagement extends PermissionSupport { * @param set the distribution set for that the actions should be canceled */ @PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY) - void cancelActionsForDistributionSet(final CancelationType cancelationType, final DistributionSet set); + void cancelActionsForDistributionSet(final ActionCancellationType cancelationType, final DistributionSet set); } \ No newline at end of file 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 354fa44fa..3d433013d 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 @@ -37,6 +37,7 @@ import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.NamedEntity; import org.eclipse.hawkbit.repository.model.Rollout; @@ -354,6 +355,14 @@ public interface RolloutManagement extends PermissionSupport { @PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY) Rollout update(@NotNull @Valid Update update); + /** + * Stop a rollout + * @param rolloutId of the rollout to be stopped + * @return stopped rollout + */ + @PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY) + Rollout stop(long rolloutId); + /** * Deletes a rollout. A rollout might be deleted asynchronously by * indicating the rollout by {@link RolloutStatus#DELETING} @@ -372,7 +381,7 @@ public interface RolloutManagement extends PermissionSupport { * canceled */ @PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY) - void cancelRolloutsForDistributionSet(DistributionSet set); + void cancelRolloutsForDistributionSet(DistributionSet set, ActionCancellationType cancelationType); /** * Triggers next group of a rollout for processing even success threshold @@ -385,6 +394,15 @@ public interface RolloutManagement extends PermissionSupport { @PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY) void triggerNextGroup(long rolloutId); + /** + * Cancels all actions that refer to a given rollout. + * + * @param cancelationType - type of cancellation - FORCE or SOFT (NONE is ignored) + * @param rollout - the rollout which actions are about to be cancelled + */ + @PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY) + void cancelActiveActionsForRollouts(final Rollout rollout, final ActionCancellationType cancelationType); + @SuperBuilder @Getter @EqualsAndHashCode(callSuper = true) diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionCancellationType.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionCancellationType.java new file mode 100644 index 000000000..507bde5f7 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionCancellationType.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2025 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.model; + +/** + * Defines if and how actions should be canceled when : + * - invalidating a distribution set + * - stopping a rollout + * - deleting a rollout + */ +public enum ActionCancellationType { + /** + * will perform a FORCE action cancellation - will put them in CANCELED state. + */ + FORCE, + + /** + * will perform a SOFT action cancellation - will put them in CANCELING state. + */ + SOFT, + + /** + * Used in distribution set invalidation - will ONLY invalidate the DS, will not change action status + */ + NONE +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetInvalidation.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetInvalidation.java index e021452ab..59731c438 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetInvalidation.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetInvalidation.java @@ -20,28 +20,16 @@ import lombok.Data; public class DistributionSetInvalidation { private Collection distributionSetIds; - private CancelationType cancelationType; - private boolean cancelRollouts; + private ActionCancellationType actionCancellationType; /** * Parametric constructor * * @param distributionSetIds defines which distribution sets should be canceled - * @param cancelationType defines if actions should be canceled - * @param cancelRollouts defines if rollouts should be canceled + * @param actionCancellationType defines if actions should be canceled */ - public DistributionSetInvalidation(final Collection distributionSetIds, final CancelationType cancelationType, - final boolean cancelRollouts) { + public DistributionSetInvalidation(final Collection distributionSetIds, final ActionCancellationType actionCancellationType) { this.distributionSetIds = distributionSetIds; - this.cancelationType = cancelationType; - this.cancelRollouts = cancelRollouts; - } - - /** - * Defines if and how actions should be canceled when invalidating a - * distribution set - */ - public enum CancelationType { - FORCE, SOFT, NONE + this.actionCancellationType = actionCancellationType; } } \ No newline at end of file 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 c02689a06..f18afdfb7 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 @@ -54,7 +54,9 @@ import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.RolloutGroup; @@ -288,10 +290,19 @@ public class JpaRolloutExecutor implements RolloutExecutor { return; } - // set soft delete - rollout.setStatus(RolloutStatus.DELETED); - rollout.setDeleted(true); - rolloutRepository.save(rollout); + finishRolloutGroups(rollout); + + rolloutManagement.cancelActiveActionsForRollouts(rollout, ActionCancellationType.FORCE); + entityManager.flush(); + + boolean hasActiveActionsLeft = actionRepository.countByRolloutIdAndActive(rollout.getId(), true) > 0; + log.trace("rollout {} has active actions left : {} ", rollout.getId(), hasActiveActionsLeft); + if (!hasActiveActionsLeft) { + // set soft delete + rollout.setStatus(RolloutStatus.DELETED); + rollout.setDeleted(true); + rolloutRepository.save(rollout); + } } private void handleStopRollout(final JpaRollout rollout) { @@ -309,18 +320,20 @@ public class JpaRolloutExecutor implements RolloutExecutor { return; } - rolloutGroupRepository.findByRolloutAndStatusNotIn(rollout, List.of(RolloutGroupStatus.FINISHED, RolloutGroupStatus.ERROR)) - .forEach(rolloutGroup -> { - rolloutGroup.setStatus(RolloutGroupStatus.FINISHED); - rolloutGroupRepository.save(rolloutGroup); - }); + finishRolloutGroups(rollout); - rollout.setStatus(RolloutStatus.FINISHED); - rolloutRepository.save(rollout); + // Soft cancel all active rollouts actions + rolloutManagement.cancelActiveActionsForRollouts(rollout, ActionCancellationType.SOFT); + // check if all actions are non-active and then finish or finish once all are processed. + boolean hasActiveActions = actionRepository.countByRolloutIdAndActiveAndStatusNot(rollout.getId(), true, Status.CANCELING) > 0; + if (!hasActiveActions) { + rollout.setStatus(RolloutStatus.STOPPED); + rolloutRepository.save(rollout); - final List groupIds = rollout.getRolloutGroups().stream().map(RolloutGroup::getId).toList(); - afterCommit.afterCommit(() -> EventPublisherHolder.getInstance().getEventPublisher().publishEvent(new RolloutStoppedEvent( - tenantAware.getCurrentTenant(), rollout.getId(), groupIds))); + final List groupIds = rollout.getRolloutGroups().stream().map(RolloutGroup::getId).toList(); + afterCommit.afterCommit(() -> EventPublisherHolder.getInstance().getEventPublisher().publishEvent(new RolloutStoppedEvent( + tenantAware.getCurrentTenant(), rollout.getId(), groupIds))); + } } private void handleReadyRollout(final Rollout rollout) { @@ -392,6 +405,14 @@ public class JpaRolloutExecutor implements RolloutExecutor { } } + private void finishRolloutGroups(final JpaRollout rollout) { + rolloutGroupRepository.findByRolloutAndStatusNotIn(rollout, List.of(RolloutGroupStatus.FINISHED, RolloutGroupStatus.ERROR)) + .forEach(rolloutGroup -> { + rolloutGroup.setStatus(RolloutGroupStatus.FINISHED); + rolloutGroupRepository.save(rolloutGroup); + }); + } + private Slice findScheduledActionsByRollout(final JpaRollout rollout) { return actionRepository.findByRolloutIdAndStatus(PageRequest.of(0, TRANSACTION_ACTIONS), rollout.getId(), Status.SCHEDULED); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDeploymentManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDeploymentManagement.java index 219bc8e3b..8e26cbe4e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDeploymentManagement.java @@ -74,11 +74,11 @@ import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; -import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.Target; @@ -239,7 +239,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl RepositoryConstants.SERVER_MESSAGE_PREFIX + "manual cancelation requested")); final Action saveAction = actionRepository.save(action); - onlineDsAssignmentStrategy.cancelAssignment(action); + onlineDsAssignmentStrategy.sendCancellationMessage(action); return saveAction; } else { @@ -516,7 +516,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl @Override @Transactional - public void cancelActionsForDistributionSet(final CancelationType cancelationType, final DistributionSet distributionSet) { + public void cancelActionsForDistributionSet(final ActionCancellationType cancelationType, final DistributionSet distributionSet) { actionRepository.findAll(ActionSpecifications.byDistributionSetIdAndActiveAndStatusIsNot(distributionSet.getId(), Status.CANCELING)) .forEach(action -> { try { @@ -529,7 +529,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl log.trace("Could not cancel action {} due to entity not found exception.", action.getId(), e); } }); - if (cancelationType == CancelationType.FORCE) { + if (cancelationType == ActionCancellationType.FORCE) { actionRepository.findAll(ActionSpecifications.byDistributionSetIdAndActive(distributionSet.getId())).forEach(action -> { try { assertTargetUpdateAllowed(action); @@ -544,10 +544,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl } } - protected ActionRepository getActionRepository() { - return actionRepository; - } - protected boolean isActionsAutocloseEnabled() { return getConfigValue(REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED, Boolean.class); } @@ -1001,4 +997,9 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl throw new EntityNotFoundException(Action.class, actionId); } } + + private Page findActiveActionsForRollout(long rolloutId, Pageable pageable) { + return actionRepository + .findAll(ActionSpecifications.byRolloutIdAndActive(rolloutId), pageable); + } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetInvalidationManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetInvalidationManagement.java index 13dc07667..389db8ef9 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetInvalidationManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetInvalidationManagement.java @@ -25,9 +25,9 @@ import org.eclipse.hawkbit.repository.exception.StopRolloutException; import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository; import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper; import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; -import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.DistributionSetInvalidationCount; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.eclipse.hawkbit.security.SystemSecurityContext; @@ -80,7 +80,7 @@ public class JpaDistributionSetInvalidationManagement implements DistributionSet public void invalidateDistributionSet(final DistributionSetInvalidation distributionSetInvalidation) { log.debug("Invalidate distribution sets {}", distributionSetInvalidation.getDistributionSetIds()); final String tenant = tenantAware.getCurrentTenant(); - if (shouldRolloutsBeCanceled(distributionSetInvalidation.getCancelationType(), distributionSetInvalidation.isCancelRollouts())) { + if (shouldRolloutsBeCanceled(distributionSetInvalidation.getActionCancellationType())) { final String handlerId = JpaRolloutManagement.createRolloutLockKey(tenant); final Lock lock = lockRegistry.obtain(handlerId); try { @@ -108,30 +108,30 @@ public class JpaDistributionSetInvalidationManagement implements DistributionSet final DistributionSetInvalidation distributionSetInvalidation) { return systemSecurityContext.runAsSystem(() -> { final Collection setIds = distributionSetInvalidation.getDistributionSetIds(); - final long rolloutsCount = shouldRolloutsBeCanceled(distributionSetInvalidation.getCancelationType(), - distributionSetInvalidation.isCancelRollouts()) ? countRolloutsForInvalidation(setIds) : 0; + final long rolloutsCount = shouldRolloutsBeCanceled(distributionSetInvalidation.getActionCancellationType()) ? countRolloutsForInvalidation(setIds) : 0; final long autoAssignmentsCount = countAutoAssignmentsForInvalidation(setIds); final long actionsCount = countActionsForInvalidation(setIds, - distributionSetInvalidation.getCancelationType()); + distributionSetInvalidation.getActionCancellationType()); return new DistributionSetInvalidationCount(rolloutsCount, autoAssignmentsCount, actionsCount); }); } - private static boolean shouldRolloutsBeCanceled(final CancelationType cancelationType, - final boolean cancelRollouts) { - return cancelationType != CancelationType.NONE || cancelRollouts; + private static boolean shouldRolloutsBeCanceled(final ActionCancellationType cancelationType) { + return cancelationType != ActionCancellationType.NONE; } - private void invalidateDistributionSetsInTransaction(final DistributionSetInvalidation distributionSetInvalidation, final String tenant) { + private void invalidateDistributionSetsInTransaction(final DistributionSetInvalidation distributionSetInvalidation, + final String tenant) { DeploymentHelper.runInNewTransaction(txManager, tenant + "-invalidateDS", status -> { - distributionSetInvalidation.getDistributionSetIds().forEach(setId -> invalidateDistributionSet( - setId, distributionSetInvalidation.getCancelationType(), distributionSetInvalidation.isCancelRollouts())); + distributionSetInvalidation.getDistributionSetIds().forEach(setId -> invalidateDistributionSet(setId, + distributionSetInvalidation.getActionCancellationType())); + return 0; }); } - private void invalidateDistributionSet(final long setId, final CancelationType cancelationType, final boolean cancelRollouts) { + private void invalidateDistributionSet(final long setId, final ActionCancellationType cancelationType) { final DistributionSet distributionSet = distributionSetManagement.getOrElseThrowException(setId); if (!distributionSet.isComplete()) { throw new IncompleteDistributionSetException( @@ -141,18 +141,18 @@ public class JpaDistributionSetInvalidationManagement implements DistributionSet log.debug("Distribution set {} marked as invalid.", setId); // rollout cancellation should only be permitted with UPDATE_ROLLOUT permission - if (shouldRolloutsBeCanceled(cancelationType, cancelRollouts)) { + if (shouldRolloutsBeCanceled(cancelationType)) { log.debug("Cancel rollouts after ds invalidation. ID: {}", setId); - rolloutManagement.cancelRolloutsForDistributionSet(distributionSet); + rolloutManagement.cancelRolloutsForDistributionSet(distributionSet, cancelationType); + } + + if (cancelationType != ActionCancellationType.NONE) { + log.debug("Cancel actions after ds invalidation. ID: {}", setId); + deploymentManagement.cancelActionsForDistributionSet(cancelationType, distributionSet); } // Do run as system to ensure all actions (even invisible) are canceled due to invalidation. systemSecurityContext.runAsSystem(() -> { - if (cancelationType != CancelationType.NONE) { - log.debug("Cancel actions after ds invalidation. ID: {}", setId); - deploymentManagement.cancelActionsForDistributionSet(cancelationType, distributionSet); - } - log.debug("Cancel auto assignments after ds invalidation. ID: {}", setId); targetFilterQueryManagement.cancelAutoAssignmentForDistributionSet(setId); return null; @@ -167,11 +167,11 @@ public class JpaDistributionSetInvalidationManagement implements DistributionSet return setIds.stream().mapToLong(targetFilterQueryManagement::countByAutoAssignDistributionSetId).sum(); } - private long countActionsForInvalidation(final Collection setIds, final CancelationType cancelationType) { + private long countActionsForInvalidation(final Collection setIds, final ActionCancellationType cancelationType) { long affectedActionsByDSInvalidation = 0; - if (cancelationType == CancelationType.FORCE) { + if (cancelationType == ActionCancellationType.FORCE) { affectedActionsByDSInvalidation = countActionsForForcedInvalidation(setIds); - } else if (cancelationType == CancelationType.SOFT) { + } else if (cancelationType == ActionCancellationType.SOFT) { affectedActionsByDSInvalidation = countActionsForSoftInvalidation(setIds); } return affectedActionsByDSInvalidation; 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 44179a49c..7feef08af 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 @@ -19,6 +19,8 @@ import java.util.function.Function; import java.util.function.UnaryOperator; import java.util.stream.Collectors; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; import jakarta.validation.ConstraintDeclarationException; import jakarta.validation.Valid; import jakarta.validation.ValidationException; @@ -30,6 +32,7 @@ import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.im.authentication.SpRole; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.QuotaManagement; +import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.RolloutApprovalStrategy; import org.eclipse.hawkbit.repository.RolloutFields; @@ -46,22 +49,30 @@ import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetExcepti import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; +import org.eclipse.hawkbit.repository.jpa.Jpa; import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaBaseEntity_; +import org.eclipse.hawkbit.repository.jpa.model.JpaAction; +import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; 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.repository.ActionRepository; +import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository; import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository; import org.eclipse.hawkbit.repository.jpa.repository.RolloutRepository; +import org.eclipse.hawkbit.repository.jpa.repository.TargetRepository; import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRolloutGroupSuccessAction; import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility; +import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications; import org.eclipse.hawkbit.repository.jpa.specifications.RolloutSpecification; import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.Rollout; @@ -75,6 +86,9 @@ import org.eclipse.hawkbit.repository.model.TotalTargetCountActionStatus; import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.utils.ObjectCopyUtil; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.utils.TenantConfigHelper; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.data.domain.Page; @@ -106,6 +120,10 @@ public class JpaRolloutManagement implements RolloutManagement { RolloutStatus.CREATING, RolloutStatus.READY, RolloutStatus.WAITING_FOR_APPROVAL, RolloutStatus.STARTING, RolloutStatus.RUNNING, RolloutStatus.PAUSED, RolloutStatus.APPROVAL_DENIED); + @Value("${org.eclipse.hawkbit.repository.jpa.management.rollout.max.actions.per.transaction:5000}") + private int MAX_ACTIONS; + + private final EntityManager entityManager; private final RolloutRepository rolloutRepository; private final RolloutGroupRepository rolloutGroupRepository; private final RolloutApprovalStrategy rolloutApprovalStrategy; @@ -113,42 +131,55 @@ public class JpaRolloutManagement implements RolloutManagement { private final RolloutStatusCache rolloutStatusCache; private final ActionRepository actionRepository; private final TargetManagement targetManagement; + private final ActionStatusRepository actionStatusRepository; private final DistributionSetManagement distributionSetManagement; + private final TenantAware tenantAware; private final TenantConfigurationManagement tenantConfigurationManagement; private final QuotaManagement quotaManagement; private final AfterTransactionCommitExecutor afterCommit; private final SystemSecurityContext systemSecurityContext; private final ContextAware contextAware; private final RepositoryProperties repositoryProperties; + private final OnlineDsAssignmentStrategy onlineDsAssignmentStrategy; protected JpaRolloutManagement( + final EntityManager entityManager, final RolloutRepository rolloutRepository, final RolloutGroupRepository rolloutGroupRepository, final RolloutApprovalStrategy rolloutApprovalStrategy, final StartNextGroupRolloutGroupSuccessAction startNextRolloutGroupAction, final RolloutStatusCache rolloutStatusCache, final ActionRepository actionRepository, + final ActionStatusRepository actionStatusRepository, + final TargetRepository targetRepository, final TargetManagement targetManagement, final DistributionSetManagement distributionSetManagement, + final TenantAware tenantAware, final TenantConfigurationManagement tenantConfigurationManagement, final QuotaManagement quotaManagement, final AfterTransactionCommitExecutor afterCommit, final SystemSecurityContext systemSecurityContext, final ContextAware contextAware, final RepositoryProperties repositoryProperties) { + this.entityManager = entityManager; this.rolloutRepository = rolloutRepository; this.rolloutGroupRepository = rolloutGroupRepository; this.rolloutApprovalStrategy = rolloutApprovalStrategy; this.startNextRolloutGroupAction = startNextRolloutGroupAction; this.rolloutStatusCache = rolloutStatusCache; this.actionRepository = actionRepository; + this.actionStatusRepository = actionStatusRepository; this.targetManagement = targetManagement; this.distributionSetManagement = distributionSetManagement; + this.tenantAware = tenantAware; this.tenantConfigurationManagement = tenantConfigurationManagement; this.quotaManagement = quotaManagement; this.afterCommit = afterCommit; this.systemSecurityContext = systemSecurityContext; this.contextAware = contextAware; this.repositoryProperties = repositoryProperties; + + this.onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, actionRepository, actionStatusRepository, + quotaManagement, this::isMultiAssignmentsEnabled, this::isConfirmationFlowEnabled, repositoryProperties); } public static String createRolloutLockKey(final String tenant) { @@ -409,27 +440,49 @@ public class JpaRolloutManagement implements RolloutManagement { @Transactional @Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) - public void delete(final long rolloutId) { + public Rollout stop(long rolloutId) { final JpaRollout jpaRollout = rolloutRepository.findById(rolloutId) .orElseThrow(() -> new EntityNotFoundException(Rollout.class, rolloutId)); - if (RolloutStatus.DELETING == jpaRollout.getStatus()) { - return; + if (!ROLLOUT_STATUS_STOPPABLE.contains(jpaRollout.getStatus())) { + log.debug("Failed to stop rollout {} because it is in {} status.", rolloutId, jpaRollout.getStatus()); + throw new RolloutIllegalStateException("Rollout can only be stopped into the following statuses " + ROLLOUT_STATUS_STOPPABLE); } - jpaRollout.setStatus(RolloutStatus.DELETING); - rolloutRepository.save(jpaRollout); + + log.debug("Stopping Rollout {}", jpaRollout.getId()); + jpaRollout.setStatus(RolloutStatus.STOPPING); + return rolloutRepository.save(jpaRollout); } @Override @Transactional - public void cancelRolloutsForDistributionSet(final DistributionSet set) { + @Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, + backoff = @Backoff(delay = Constants.TX_RT_DELAY)) + public void delete(final long rolloutId) { + final JpaRollout jpaRollout = rolloutRepository.findById(rolloutId) + .orElseThrow(() -> new EntityNotFoundException(Rollout.class, rolloutId)); + this.delete0(jpaRollout); + } + + @Override + @Transactional + public void cancelRolloutsForDistributionSet(final DistributionSet set, final ActionCancellationType cancelationType) { // stop all rollouts for this distribution set - rolloutRepository.findByDistributionSetAndStatusIn(set, ROLLOUT_STATUS_STOPPABLE).forEach(rollout -> { - final JpaRollout jpaRollout = (JpaRollout) rollout; - jpaRollout.setStatus(RolloutStatus.STOPPING); - rolloutRepository.save(jpaRollout); - log.debug("Rollout {} stopped", jpaRollout.getId()); - }); + if (cancelationType.equals(ActionCancellationType.SOFT)) { + rolloutRepository.findByDistributionSetAndStatusIn(set, ROLLOUT_STATUS_STOPPABLE).forEach(rollout -> { + final JpaRollout jpaRollout = (JpaRollout) rollout; + jpaRollout.setStatus(RolloutStatus.STOPPING); + rolloutRepository.save(jpaRollout); + log.debug("Rollout {} stopping", jpaRollout.getId()); + }); + } else if (cancelationType.equals(ActionCancellationType.FORCE)) { + // Use same status filter here like in the soft case ? Seems they make sense + rolloutRepository.findByDistributionSetAndStatusIn(set, ROLLOUT_STATUS_STOPPABLE).forEach(rollout -> { + final JpaRollout jpaRollout = (JpaRollout) rollout; + this.delete0(jpaRollout); + log.debug("Rollout {} deleting", jpaRollout.getId()); + }); + } } @Override @@ -459,6 +512,124 @@ public class JpaRolloutManagement implements RolloutManagement { startNextRolloutGroupAction.exec(rollout, latestRunning); } + @Override + @Transactional + public void cancelActiveActionsForRollouts(Rollout rollout, ActionCancellationType cancelationType) { + // check cancellation type + if (ActionCancellationType.FORCE.equals(cancelationType)) { + forceQuitActionsOfRollout(rollout); + } else if (ActionCancellationType.SOFT.equals(cancelationType)) { + softCancelActionsOfRollout(rollout); + } + } + + private void softCancelActionsOfRollout(final Rollout rollout) { + final List actions = actionRepository.findAll( + ActionSpecifications + .byRolloutIdAndActiveAndStatusIsNot(rollout.getId(), + List.of(Action.Status.CANCELING)), // avoid cancelling state here, because it is count as still active + Pageable.ofSize(MAX_ACTIONS)) + .getContent(); + log.info("Found {} active actions for rollout {}, performing soft cancel.", actions.size(), rollout.getId()); + + storeActionsAndStatuses(actions, Action.Status.CANCELING); + + // send cancellation messages to event publisher + onlineDsAssignmentStrategy.sendCancellationMessages(actions, tenantAware.getCurrentTenant()); + } + + private void forceQuitActionsOfRollout(final Rollout rollout) { + final List actions = findActiveActionsForRollout(rollout.getId(), Pageable.ofSize(MAX_ACTIONS)) + .getContent(); + log.info("Found {} active actions for rollout {}", actions.size(), rollout.getId()); + + storeActionsAndStatuses(actions, Action.Status.CANCELED); + + // find next active actions - filter by targetId list and isActive + final List targetIds = actions.stream() + .map(action -> action.getTarget().getId()) + .toList(); + entityManager.flush(); + + int modifiedRows = updateTargetAssignedDsWithFirstActiveAction(targetIds); + log.debug("Updated {} targets with their previously active action", modifiedRows); + + // if no active actions + // set assignedDs to previously installedDs and status to IN_SYNC + // otherwise set assigned ds to the active action ... + modifiedRows = updateTargetAssignedDsWithInstalledIfNoActiveActions(targetIds); + log.debug("Updated assignDs to previously installed to {} number of targets.", modifiedRows); + } + + private void storeActionsAndStatuses(List actions, Action.Status status) { + final List cancellingStatuses = new ArrayList<>(actions.size()); + final long currentTimestamp = System.currentTimeMillis(); + final boolean active = Action.Status.CANCELING.equals(status); + final String typeOfCancellation = active ? "cancellation" : "force quit"; + + actions.forEach(action -> { + action.setStatus(status); + action.setActive(active); + + JpaActionStatus actionStatus = new JpaActionStatus(); + actionStatus.setAction(action); + actionStatus.setStatus(status); + actionStatus.setOccurredAt(currentTimestamp); + actionStatus.addMessage(RepositoryConstants.SERVER_MESSAGE_PREFIX + "A " + typeOfCancellation + " has been performed by server."); + cancellingStatuses.add(actionStatus); + }); + + actionStatusRepository.saveAll(cancellingStatuses); + actionRepository.saveAll(actions); + } + + private int updateTargetAssignedDsWithFirstActiveAction(List targetIds) { + final Query updateQuery = entityManager.createNativeQuery( + "UPDATE sp_target t " + + "SET t.assigned_distribution_set = ( " + + "SELECT a.distribution_set" + + " FROM sp_action a" + + " WHERE a.target = t.id AND a.active = 1" + + " ORDER BY a.id ASC" + + " LIMIT 1" + + ") " + + "WHERE t.id IN (" + Jpa.formatNativeQueryInClause("tid", targetIds) + ")" + ); + Jpa.setNativeQueryInParameter(updateQuery, "tid", targetIds); + final int updated = updateQuery.executeUpdate(); + log.info("{} of target assigned distribution values updated for tenant {}", + updated, tenantAware.getCurrentTenant()); + return updated; + } + + private int updateTargetAssignedDsWithInstalledIfNoActiveActions(List targetIds) { + final Query updateQuery = entityManager.createNativeQuery( + "UPDATE sp_target t " + + "SET t.assigned_distribution_set = t.installed_distribution_set, t.update_status = 1 " + + "WHERE t.id IN (" + Jpa.formatNativeQueryInClause("tid", targetIds) + ") " + + " AND (SELECT count(*) FROM sp_action a " + + " WHERE a.target=t.id and a.active=1) = 0" + ); + Jpa.setNativeQueryInParameter(updateQuery, "tid", targetIds); + final int updated = updateQuery.executeUpdate(); + log.info("{} of target assigned distribution set to previously installed distribution value for tenant {}", + updated, tenantAware.getCurrentTenant()); + return updated; + } + + private Page findActiveActionsForRollout(long rolloutId, Pageable pageable) { + return actionRepository + .findAll(ActionSpecifications.byRolloutIdAndActive(rolloutId), pageable); + } + + private void delete0(final JpaRollout jpaRollout) { + if (RolloutStatus.DELETING == jpaRollout.getStatus()) { + return; + } + jpaRollout.setStatus(RolloutStatus.DELETING); + rolloutRepository.save(jpaRollout); + } + private Page appendStatusDetails(final Page rollouts) { final List rolloutIds = rollouts.getContent().stream().map(Rollout::getId).toList(); final Map> allStatesForRollout = getStatusCountItemForRollout(rolloutIds); @@ -851,5 +1022,13 @@ public class JpaRolloutManagement implements RolloutManagement { return new TargetCount(totalTargets, baseFilter); } + private boolean isMultiAssignmentsEnabled() { + return TenantConfigHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).isMultiAssignmentsEnabled(); + } + + private boolean isConfirmationFlowEnabled() { + return TenantConfigHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).isConfirmationFlowEnabled(); + } + private record TargetCount(long total, String filter) {} } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OnlineDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OnlineDsAssignmentStrategy.java index 5a84532ef..855c0dca1 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OnlineDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OnlineDsAssignmentStrategy.java @@ -176,7 +176,7 @@ class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { } } - void cancelAssignment(final JpaAction action) { + void sendCancellationMessage(final JpaAction action) { if (isMultiAssignmentsEnabled()) { sendMultiActionCancelEvent(action); } else { @@ -184,6 +184,14 @@ class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { } } + void sendCancellationMessages(final List actions, final String tenant) { + if(isMultiAssignmentsEnabled()) { + sendMultiActionCancelEvent(tenant, Collections.unmodifiableList(actions)); + } else { + actions.forEach(this::cancelAssignDistributionSetEvent); + } + } + private static Stream filterCancellations(final List actions) { return actions.stream().filter(action -> { final Status actionStatus = action.getStatus(); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/repository/ActionRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/repository/ActionRepository.java index 8b4a58788..da317975a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/repository/ActionRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/repository/ActionRepository.java @@ -183,6 +183,25 @@ public interface ActionRepository extends BaseEntityRepository { */ Long countByRolloutIdAndStatus(Long rolloutId, Action.Status status); + /** + * Returns the number of active/non-active actions for a rollout + * + * @param rolloutId - the ID of the rollout the actions belong to + * @param active - wether the actions should be active or not + * @return number of actions which match the criteria + */ + Long countByRolloutIdAndActive(Long rolloutId, boolean active); + + /** + * Returns the number of active/non-active actions for a rollout which status is not the provided one. + * + * @param rolloutId - the ID of the rollout the actions belong to + * @param active - wether the actions should be active or not + * @param status - the status that matched actions should not be. + * @return number of actions which match the criteria + */ + Long countByRolloutIdAndActiveAndStatusNot(Long rolloutId, boolean active, Action.Status status); + /** * Returns {@code true} if actions for the given rollout exists, otherwise {@code false} *

diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/ActionSpecifications.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/ActionSpecifications.java index c20936df7..d7ac2ee3d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/ActionSpecifications.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/ActionSpecifications.java @@ -28,6 +28,8 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; import org.eclipse.hawkbit.repository.model.Action; import org.springframework.data.jpa.domain.Specification; +import java.util.List; + /** * Utility class for {@link Action}s {@link Specification}s. The class provides Spring Data JPQL Specifications. */ @@ -84,6 +86,21 @@ public final class ActionSpecifications { cb.equal(root.get(JpaAction_.active), true)); } + public static Specification byRolloutIdAndActive(final Long rolloutId) { + return (root, query, cb) -> cb.and( + cb.equal(root.get(JpaAction_.rollout).get(AbstractJpaBaseEntity_.id), rolloutId), + cb.equal(root.get(JpaAction_.active), true) + ); + } + + public static Specification byRolloutIdAndActiveAndStatusIsNot(final Long rolloutId, final List statuses) { + return (root, query, cb) -> cb.and( + cb.equal(root.get(JpaAction_.rollout).get(AbstractJpaBaseEntity_.id), rolloutId), + cb.equal(root.get(JpaAction_.active), true), + cb.not(root.get(JpaAction_.status).in(statuses)) + ); + } + public static Specification byDistributionSetIdAndActiveAndStatusIsNot(final Long distributionSetId, final Action.Status status) { return (root, query, cb) -> cb.and( cb.equal(root.get(JpaAction_.distributionSet).get(AbstractJpaBaseEntity_.id), distributionSetId), diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ConcurrentDistributionSetInvalidationTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ConcurrentDistributionSetInvalidationTest.java index 4cdc00b48..03b889881 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ConcurrentDistributionSetInvalidationTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ConcurrentDistributionSetInvalidationTest.java @@ -25,9 +25,9 @@ import org.eclipse.hawkbit.repository.exception.StopRolloutException; import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository; import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; -import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupErrorAction; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupErrorCondition; @@ -78,7 +78,7 @@ class ConcurrentDistributionSetInvalidationTest extends AbstractJpaIntegrationTe () -> rolloutGroupManagement.findByRollout(rollout.getId(), PAGE).getSize() > 0))); final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( - Collections.singletonList(distributionSet.getId()), CancelationType.SOFT, true); + Collections.singletonList(distributionSet.getId()), ActionCancellationType.SOFT); assertThatExceptionOfType(StopRolloutException.class) .as("Invalidation of distributionSet should throw an exception") .isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation)); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DeploymentManagementSecurityTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DeploymentManagementSecurityTest.java index 3c91d36a4..c646bff2b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DeploymentManagementSecurityTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DeploymentManagementSecurityTest.java @@ -17,8 +17,8 @@ import org.eclipse.hawkbit.im.authentication.SpRole; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.DeploymentRequest; -import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Pageable; @@ -294,7 +294,7 @@ class DeploymentManagementSecurityTest extends AbstractJpaIntegrationTest { void cancelActionsForDistributionSetPermissionsCheck() { assertPermissions(() -> { deploymentManagement.cancelActionsForDistributionSet( - DistributionSetInvalidation.CancelationType.FORCE, new JpaDistributionSet()); + ActionCancellationType.FORCE, new JpaDistributionSet()); return null; }, List.of(SpPermission.UPDATE_TARGET)); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetInvalidationManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetInvalidationManagementTest.java index aa74784c1..d72ae8c12 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetInvalidationManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetInvalidationManagementTest.java @@ -15,6 +15,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.util.Collections; import java.util.List; +import lombok.extern.slf4j.Slf4j; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; @@ -22,13 +23,12 @@ import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications; import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; -import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.DistributionSetInvalidationCount; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; -import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; @@ -43,6 +43,7 @@ import org.springframework.data.repository.query.Param; * Feature: Component Tests - Repository
* Story: Distribution set invalidation management */ +@Slf4j class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTest { /** @@ -53,8 +54,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe final InvalidationTestData invalidationTestData = createInvalidationTestData("verifyInvalidateDistributionSetStopAutoAssignment"); final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( - Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE, - false); + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.NONE); final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement .countEntitiesForInvalidation(distributionSetInvalidation); assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 0, 0); @@ -76,19 +76,18 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe } /** - * Verify invalidation of distribution sets that removes distribution sets from auto assignments and stops rollouts + * Verify invalidation of distribution sets that removes distribution sets from auto assignments but does not stop rollouts */ @Test - void verifyInvalidateDistributionSetStopRollouts() { + void verifyInvalidateDistributionSetDoesNotStopRollouts() { final InvalidationTestData invalidationTestData = createInvalidationTestData( "verifyInvalidateDistributionSetStopRollouts"); final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( - Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE, - true); + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.NONE); final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement .countEntitiesForInvalidation(distributionSetInvalidation); - assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 0, 1); + assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 0, 0); distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation); rolloutHandler.handleAll(); @@ -96,9 +95,9 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get() .getAutoAssignDistributionSet()).isNull(); assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus()) - .isEqualTo(RolloutStatus.FINISHED); + .isEqualTo(RolloutStatus.READY); + assertNoScheduledActionsExist(invalidationTestData.getRollout()); - assertRolloutGroupsAreFinished(invalidationTestData.getRollout()); for (final Target target : invalidationTestData.getTargets()) { // if status is pending, the assignment has not been canceled assertThat( @@ -118,8 +117,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe "verifyInvalidateDistributionSetStopAllAndForceCancel"); final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( - Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.FORCE, - true); + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.FORCE); final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement .countEntitiesForInvalidation(distributionSetInvalidation); assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 5, 1); @@ -129,16 +127,10 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get() .getAutoAssignDistributionSet()).isNull(); - assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus()) - .isEqualTo(RolloutStatus.FINISHED); + // rollout should be deleted when force invalidation + assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId())).isEmpty(); assertNoScheduledActionsExist(invalidationTestData.getRollout()); - assertRolloutGroupsAreFinished(invalidationTestData.getRollout()); - for (final Target target : invalidationTestData.getTargets()) { - assertThat(targetRepository.findById(target.getId()).get().getUpdateStatus()) - .isEqualTo(TargetUpdateStatus.IN_SYNC); - assertThat(findActionsByTarget(target)).hasSize(1); - assertThat(findActionsByTarget(target).get(0).getStatus()).isEqualTo(Status.CANCELED); - } + assertAllRolloutActionsAreCancelled(invalidationTestData.getRollout()); } /** @@ -149,7 +141,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe final InvalidationTestData invalidationTestData = createInvalidationTestData("verifyInvalidateDistributionSetStopAll"); final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( - Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.SOFT,true); + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.SOFT); final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement .countEntitiesForInvalidation(distributionSetInvalidation); assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 5, 1); @@ -176,7 +168,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe final DistributionSet distributionSet = testdataFactory.createIncompleteDistributionSet(); final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( - List.of(distributionSet.getId()), CancelationType.SOFT, true); + List.of(distributionSet.getId()), ActionCancellationType.SOFT); assertThatExceptionOfType(IncompleteDistributionSetException.class) .as("Incomplete distributionSet should throw an exception") .isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation)); @@ -193,7 +185,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet(); distributionSetInvalidationManagement.invalidateDistributionSet( new DistributionSetInvalidation(Collections.singletonList(distributionSet.getId()), - CancelationType.SOFT, true)); + ActionCancellationType.SOFT)); } /** @@ -206,8 +198,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe .runAsSystem(() -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAuthority")); distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation( - Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE, - false)); + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.NONE)); assertThat( distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid()) .isFalse(); @@ -223,14 +214,13 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe () -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAndUpdateTargetAuthority")); final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( - List.of(invalidationTestData.getDistributionSet().getId()), CancelationType.SOFT, true); + List.of(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.SOFT); assertThatExceptionOfType(InsufficientPermissionException.class) .as("Insufficient permission exception expected") .isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation)); distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation( - Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE, - false)); + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.NONE)); assertThat( distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid()) .isFalse(); @@ -246,8 +236,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe () -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAndUpdateTargetAuthority")); distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation( - Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.SOFT, - true)); + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.SOFT)); assertThat( distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid()) .isFalse(); @@ -259,10 +248,10 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe .isZero(); } - private void assertRolloutGroupsAreFinished(final Rollout rollout) { - assertThat(rolloutGroupRepository.findByRolloutId(rollout.getId(), PAGE)) - .isNotEmpty() - .allMatch(rolloutGroup -> rolloutGroup.getStatus().equals(RolloutGroupStatus.FINISHED)); + private void assertAllRolloutActionsAreCancelled(final Rollout rollout) { + assertThat( + actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), Status.CANCELED).getTotalElements()) + .isZero(); } private InvalidationTestData createInvalidationTestData(final String testName) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java index f3195680e..8107bbaf7 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java @@ -50,11 +50,11 @@ import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThis import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetFilter; import org.eclipse.hawkbit.repository.model.DistributionSetFilter.DistributionSetFilterBuilder; import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; -import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.DistributionSetTag; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.NamedEntity; @@ -482,7 +482,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest { final Long softwareModuleId = testdataFactory.createSoftwareModuleOs().getId(); distributionSetManagement.assignSoftwareModules(distributionSetId, singletonList(softwareModuleId)); distributionSetInvalidationManagement.invalidateDistributionSet( - new DistributionSetInvalidation(singletonList(distributionSetId), CancelationType.NONE, false)); + new DistributionSetInvalidation(singletonList(distributionSetId), ActionCancellationType.NONE)); assertThatExceptionOfType(InvalidDistributionSetException.class) .as("Invalid distributionSet should throw an exception").isThrownBy(() -> distributionSetManagement @@ -876,7 +876,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest { distributionSetManagement.createMetadata(dsId, Map.of(knownKey1, knownValue)); distributionSetInvalidationManagement.invalidateDistributionSet( - new DistributionSetInvalidation(singletonList(dsId), CancelationType.NONE, false)); + new DistributionSetInvalidation(singletonList(dsId), ActionCancellationType.NONE)); // assert that no new metadata can be created assertThatExceptionOfType(InvalidDistributionSetException.class) diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementSecurityTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementSecurityTest.java index de77490b5..01a1d253d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementSecurityTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementSecurityTest.java @@ -20,7 +20,9 @@ import org.eclipse.hawkbit.repository.RolloutManagement.Create; import org.eclipse.hawkbit.repository.RolloutManagement.GroupCreate; import org.eclipse.hawkbit.repository.RolloutManagement.Update; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder; import org.eclipse.hawkbit.repository.test.util.TestdataFactory; @@ -113,7 +115,7 @@ class RolloutManagementSecurityTest extends AbstractJpaIntegrationTest { DistributionSetManagement.Create.builder().type(defaultDsType()).name("name").version("1.0.0").build(); final DistributionSet ds = distributionSetManagement.create(dsCreate); assertPermissions(() -> { - rolloutManagement.cancelRolloutsForDistributionSet(ds); + rolloutManagement.cancelRolloutsForDistributionSet(ds, ActionCancellationType.SOFT); return null; }, List.of(SpPermission.UPDATE_ROLLOUT, SpPermission.READ_REPOSITORY, SpPermission.CREATE_REPOSITORY)); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java index 3518a8a2e..deaf2ba9f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java @@ -1914,11 +1914,11 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { @Expect(type = TargetUpdatedEvent.class, count = 2), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 10), - @Expect(type = ActionUpdatedEvent.class, count = 2), + @Expect(type = ActionUpdatedEvent.class, count = 4), @Expect(type = RolloutCreatedEvent.class, count = 1), @Expect(type = RolloutUpdatedEvent.class, count = 6), @Expect(type = RolloutDeletedEvent.class, count = 1), - @Expect(type = RolloutGroupUpdatedEvent.class, count = 11), + @Expect(type = RolloutGroupUpdatedEvent.class, count = 16), @Expect(type = RolloutGroupCreatedEvent.class, count = 5) }) void deleteRolloutWhichHasBeenStartedBeforeIsSoftDeleted() { final int amountTargetsForRollout = 10; @@ -1962,8 +1962,8 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { // verify that all scheduled actions are deleted assertThat(actionRepository.findByRolloutIdAndStatus(PAGE, deletedRollout.getId(), Status.SCHEDULED) .getNumberOfElements()).isZero(); - // verify that all running actions keep running - assertThat(actionRepository.findByRolloutIdAndStatus(PAGE, deletedRollout.getId(), Status.RUNNING) + // verify that all running actions are force cancelled + assertThat(actionRepository.findByRolloutIdAndStatus(PAGE, deletedRollout.getId(), Status.CANCELED) .getNumberOfElements()).isEqualTo(2); } 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 5d85f994a..e4df9a897 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 @@ -52,6 +52,7 @@ import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionStatusCreate; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionCancellationType; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.ArtifactUpload; @@ -59,7 +60,6 @@ import org.eclipse.hawkbit.repository.model.BaseEntity; import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; -import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.DistributionSetTag; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.NamedEntity; @@ -1082,6 +1082,19 @@ public class TestdataFactory { createDistributionSet(prefix), "50", "5"); } + /** + * Create {@link Rollout} with a new {@link DistributionSet} and {@link Target}s. + * + * @return created {@link Rollout} + */ + public Rollout createAndStartRollout() { + return startAndReloadRollout(createRollout()); + } + + public Rollout startRollout(final Rollout rollout) { + return startAndReloadRollout(rollout); + } + /** * Create the data for a simple rollout scenario * @@ -1212,7 +1225,7 @@ public class TestdataFactory { public DistributionSet createAndInvalidateDistributionSet() { final DistributionSet distributionSet = createDistributionSet(); distributionSetInvalidationManagement.invalidateDistributionSet( - new DistributionSetInvalidation(List.of(distributionSet.getId()), CancelationType.NONE, false)); + new DistributionSetInvalidation(List.of(distributionSet.getId()), ActionCancellationType.NONE)); return distributionSetManagement.get(distributionSet.getId()).orElseThrow(); }