Introduce Stop of a rollout (#2595)

* Stop of a rollout feature

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* remove some test comments

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* make stop transactional

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* attempt to fix hibernate failed tests

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* fix some sonar issues

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* changes after review

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* fix build

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* fixes after review

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* finish all rollout groups on deletion of rollout

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* refactor finishing groups

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* fix RolloutManagementTest

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

---------

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>
This commit is contained in:
Stanislav Trailov
2025-08-12 17:13:50 +03:00
committed by GitHub
parent 4566702030
commit 45cd012532
25 changed files with 581 additions and 140 deletions

View File

@@ -26,6 +26,4 @@ public class MgmtInvalidateDistributionSetRequestBody {
@Schema(description = "Type of cancelation for actions referring to the given distribution set") @Schema(description = "Type of cancelation for actions referring to the given distribution set")
private MgmtCancelationType actionCancelationType; private MgmtCancelationType actionCancelationType;
@Schema(description = "Defines if rollouts referring to this distribution set should be canceled", example = "true")
private boolean cancelRollouts;
} }

View File

@@ -348,6 +348,39 @@ public interface MgmtRolloutRestApi {
produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE })
ResponseEntity<Void> pause(@PathVariable("rolloutId") Long rolloutId); ResponseEntity<Void> 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<Void> stop(@PathVariable("rolloutId") Long rolloutId);
/** /**
* Handles the DELETE request for deleting a rollout. * Handles the DELETE request for deleting a rollout.
* *

View File

@@ -360,8 +360,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
distributionSetInvalidationManagement distributionSetInvalidationManagement
.invalidateDistributionSet( .invalidateDistributionSet(
new DistributionSetInvalidation(Collections.singletonList(distributionSetId), new DistributionSetInvalidation(Collections.singletonList(distributionSetId),
MgmtRestModelMapper.convertCancelationType(invalidateRequestBody.getActionCancelationType()), MgmtRestModelMapper.convertCancelationType(invalidateRequestBody.getActionCancelationType())));
invalidateRequestBody.isCancelRollouts()));
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
} }

View File

@@ -185,6 +185,13 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@Override
@AuditLog(entity = "Rollout", type = AuditLog.Type.UPDATE, description = "Stop Rollout")
public ResponseEntity<Void> stop(Long rolloutId) {
this.rolloutManagement.stop(rolloutId);
return ResponseEntity.ok().build();
}
@Override @Override
@AuditLog(entity = "Rollout", type = AuditLog.Type.DELETE, description = "Delete Rollout") @AuditLog(entity = "Rollout", type = AuditLog.Type.DELETE, description = "Delete Rollout")
public ResponseEntity<Void> delete(final Long rolloutId) { public ResponseEntity<Void> delete(final Long rolloutId) {

View File

@@ -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.MgmtActionType;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtCancelationType; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtCancelationType;
import org.eclipse.hawkbit.repository.model.Action.ActionType; 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.NamedEntity;
import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity; import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity;
import org.eclipse.hawkbit.repository.model.Type; 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 * @param cancelationType the repository representation of the cancellation type
* @return <null> or the REST cancellation type * @return <null> or the REST cancellation type
*/ */
public static CancelationType convertCancelationType(final MgmtCancelationType cancelationType) { public static ActionCancellationType convertCancelationType(final MgmtCancelationType cancelationType) {
if (cancelationType == null) { if (cancelationType == null) {
return null; return null;
} }
return switch (cancelationType) { return switch (cancelationType) {
case SOFT -> CancelationType.SOFT; case SOFT -> ActionCancellationType.SOFT;
case FORCE -> CancelationType.FORCE; case FORCE -> ActionCancellationType.FORCE;
case NONE -> CancelationType.NONE; case NONE -> ActionCancellationType.NONE;
}; };
} }

View File

@@ -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 * Verify invalidation of distribution sets that removes distribution sets from auto assignments, stops rollouts and cancels assignments
*/ */
@Test @Test
void invalidateDistributionSet() throws Exception { void softInvalidateDistributionSet() throws Exception {
DistributionSet distributionSet = testdataFactory.createDistributionSet(); DistributionSet distributionSet = testdataFactory.createDistributionSet();
final List<Target> targets = testdataFactory.createTargets(5, "invalidateDistributionSet"); final List<Target> targets = testdataFactory.createTargets(5, "invalidateDistributionSet");
// the distribution set is locked and the old instance become stale // the distribution set is locked and the old instance become stale
@@ -1697,7 +1697,6 @@ class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegrationTe
final JSONObject jsonObject = new JSONObject(); final JSONObject jsonObject = new JSONObject();
jsonObject.put("actionCancelationType", "soft"); jsonObject.put("actionCancelationType", "soft");
jsonObject.put("cancelRollouts", true);
mvc.perform(post("/rest/v1/distributionsets/{ds}/invalidate", distributionSet.getId()) mvc.perform(post("/rest/v1/distributionsets/{ds}/invalidate", distributionSet.getId())
.content(jsonObject.toString()).contentType(MediaType.APPLICATION_JSON)) .content(jsonObject.toString()).contentType(MediaType.APPLICATION_JSON))
@@ -1706,7 +1705,11 @@ class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegrationTe
assertThat(targetFilterQueryManagement.get(targetFilterQuery.getId()).get().getAutoAssignDistributionSet()) assertThat(targetFilterQueryManagement.get(targetFilterQuery.getId()).get().getAutoAssignDistributionSet())
.isNull(); .isNull();
assertThat(rolloutManagement.get(rollout.getId()).get().getStatus()).isIn(RolloutStatus.STOPPING, 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) { for (final Target target : targets) {
assertThat(targetManagement.get(target.getId()).get().getUpdateStatus()) assertThat(targetManagement.get(target.getId()).get().getUpdateStatus())
.isEqualTo(TargetUpdateStatus.PENDING); .isEqualTo(TargetUpdateStatus.PENDING);
@@ -1717,6 +1720,69 @@ class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegrationTe
} }
} }
@Test
void forceInvalidateDistributionSet() throws Exception {
DistributionSet distributionSet = testdataFactory.createDistributionSet();
final List<Target> 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<Target> 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. * Tests the lock. It is verified that the distribution set can be marked as locked through update operation.
*/ */

View File

@@ -31,6 +31,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
@@ -1461,6 +1462,55 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
.andExpect(jsonPath("$.deleted", equalTo(true))); .andExpect(jsonPath("$.deleted", equalTo(true)));
assertStatusIs(rollout, RolloutStatus.DELETED); assertStatusIs(rollout, RolloutStatus.DELETED);
List<Action> 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<RolloutGroup> 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<Action> 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")));
} }
/** /**

View File

@@ -31,12 +31,12 @@ import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.Status; 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.ActionStatus;
import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder; import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder;
import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; 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.DistributionSetType;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
import org.eclipse.hawkbit.repository.model.Target; 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 * @param set the distribution set for that the actions should be canceled
*/ */
@PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY) @PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY)
void cancelActionsForDistributionSet(final CancelationType cancelationType, final DistributionSet set); void cancelActionsForDistributionSet(final ActionCancellationType cancelationType, final DistributionSet set);
} }

View File

@@ -37,6 +37,7 @@ import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException;
import org.eclipse.hawkbit.repository.model.Action; 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.DistributionSet;
import org.eclipse.hawkbit.repository.model.NamedEntity; import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout;
@@ -354,6 +355,14 @@ public interface RolloutManagement extends PermissionSupport {
@PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY) @PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY)
Rollout update(@NotNull @Valid Update update); 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 * Deletes a rollout. A rollout might be deleted asynchronously by
* indicating the rollout by {@link RolloutStatus#DELETING} * indicating the rollout by {@link RolloutStatus#DELETING}
@@ -372,7 +381,7 @@ public interface RolloutManagement extends PermissionSupport {
* canceled * canceled
*/ */
@PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY) @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 * 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) @PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY)
void triggerNextGroup(long rolloutId); 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 @SuperBuilder
@Getter @Getter
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)

View File

@@ -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
}

View File

@@ -20,28 +20,16 @@ import lombok.Data;
public class DistributionSetInvalidation { public class DistributionSetInvalidation {
private Collection<Long> distributionSetIds; private Collection<Long> distributionSetIds;
private CancelationType cancelationType; private ActionCancellationType actionCancellationType;
private boolean cancelRollouts;
/** /**
* Parametric constructor * Parametric constructor
* *
* @param distributionSetIds defines which distribution sets should be canceled * @param distributionSetIds defines which distribution sets should be canceled
* @param cancelationType defines if actions should be canceled * @param actionCancellationType defines if actions should be canceled
* @param cancelRollouts defines if rollouts should be canceled
*/ */
public DistributionSetInvalidation(final Collection<Long> distributionSetIds, final CancelationType cancelationType, public DistributionSetInvalidation(final Collection<Long> distributionSetIds, final ActionCancellationType actionCancellationType) {
final boolean cancelRollouts) {
this.distributionSetIds = distributionSetIds; this.distributionSetIds = distributionSetIds;
this.cancelationType = cancelationType; this.actionCancellationType = actionCancellationType;
this.cancelRollouts = cancelRollouts;
}
/**
* Defines if and how actions should be canceled when invalidating a
* distribution set
*/
public enum CancelationType {
FORCE, SOFT, NONE
} }
} }

View File

@@ -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;
import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.Action.Status; 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.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation;
import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroup;
@@ -288,11 +290,20 @@ public class JpaRolloutExecutor implements RolloutExecutor {
return; return;
} }
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 // set soft delete
rollout.setStatus(RolloutStatus.DELETED); rollout.setStatus(RolloutStatus.DELETED);
rollout.setDeleted(true); rollout.setDeleted(true);
rolloutRepository.save(rollout); rolloutRepository.save(rollout);
} }
}
private void handleStopRollout(final JpaRollout rollout) { private void handleStopRollout(final JpaRollout rollout) {
log.debug("handleStopRollout called for {}", rollout.getId()); log.debug("handleStopRollout called for {}", rollout.getId());
@@ -309,19 +320,21 @@ public class JpaRolloutExecutor implements RolloutExecutor {
return; return;
} }
rolloutGroupRepository.findByRolloutAndStatusNotIn(rollout, List.of(RolloutGroupStatus.FINISHED, RolloutGroupStatus.ERROR)) finishRolloutGroups(rollout);
.forEach(rolloutGroup -> {
rolloutGroup.setStatus(RolloutGroupStatus.FINISHED);
rolloutGroupRepository.save(rolloutGroup);
});
rollout.setStatus(RolloutStatus.FINISHED); // 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); rolloutRepository.save(rollout);
final List<Long> groupIds = rollout.getRolloutGroups().stream().map(RolloutGroup::getId).toList(); final List<Long> groupIds = rollout.getRolloutGroups().stream().map(RolloutGroup::getId).toList();
afterCommit.afterCommit(() -> EventPublisherHolder.getInstance().getEventPublisher().publishEvent(new RolloutStoppedEvent( afterCommit.afterCommit(() -> EventPublisherHolder.getInstance().getEventPublisher().publishEvent(new RolloutStoppedEvent(
tenantAware.getCurrentTenant(), rollout.getId(), groupIds))); tenantAware.getCurrentTenant(), rollout.getId(), groupIds)));
} }
}
private void handleReadyRollout(final Rollout rollout) { private void handleReadyRollout(final Rollout rollout) {
if (rollout.getStartAt() != null && rollout.getStartAt() <= System.currentTimeMillis()) { if (rollout.getStartAt() != null && rollout.getStartAt() <= System.currentTimeMillis()) {
@@ -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<JpaAction> findScheduledActionsByRollout(final JpaRollout rollout) { private Slice<JpaAction> findScheduledActionsByRollout(final JpaRollout rollout) {
return actionRepository.findByRolloutIdAndStatus(PageRequest.of(0, TRANSACTION_ACTIONS), rollout.getId(), return actionRepository.findByRolloutIdAndStatus(PageRequest.of(0, TRANSACTION_ACTIONS), rollout.getId(),
Status.SCHEDULED); Status.SCHEDULED);

View File

@@ -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;
import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.Action.Status; 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.ActionStatus;
import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; 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.DistributionSetType;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
import org.eclipse.hawkbit.repository.model.Target; 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")); RepositoryConstants.SERVER_MESSAGE_PREFIX + "manual cancelation requested"));
final Action saveAction = actionRepository.save(action); final Action saveAction = actionRepository.save(action);
onlineDsAssignmentStrategy.cancelAssignment(action); onlineDsAssignmentStrategy.sendCancellationMessage(action);
return saveAction; return saveAction;
} else { } else {
@@ -516,7 +516,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
@Override @Override
@Transactional @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)) actionRepository.findAll(ActionSpecifications.byDistributionSetIdAndActiveAndStatusIsNot(distributionSet.getId(), Status.CANCELING))
.forEach(action -> { .forEach(action -> {
try { 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); 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 -> { actionRepository.findAll(ActionSpecifications.byDistributionSetIdAndActive(distributionSet.getId())).forEach(action -> {
try { try {
assertTargetUpdateAllowed(action); assertTargetUpdateAllowed(action);
@@ -544,10 +544,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
} }
} }
protected ActionRepository getActionRepository() {
return actionRepository;
}
protected boolean isActionsAutocloseEnabled() { protected boolean isActionsAutocloseEnabled() {
return getConfigValue(REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED, Boolean.class); 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); throw new EntityNotFoundException(Action.class, actionId);
} }
} }
private Page<JpaAction> findActiveActionsForRollout(long rolloutId, Pageable pageable) {
return actionRepository
.findAll(ActionSpecifications.byRolloutIdAndActive(rolloutId), pageable);
}
} }

View File

@@ -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.repository.ActionRepository;
import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper; import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper;
import org.eclipse.hawkbit.repository.model.Action.Status; 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.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; 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.DistributionSetInvalidationCount;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.security.SystemSecurityContext;
@@ -80,7 +80,7 @@ public class JpaDistributionSetInvalidationManagement implements DistributionSet
public void invalidateDistributionSet(final DistributionSetInvalidation distributionSetInvalidation) { public void invalidateDistributionSet(final DistributionSetInvalidation distributionSetInvalidation) {
log.debug("Invalidate distribution sets {}", distributionSetInvalidation.getDistributionSetIds()); log.debug("Invalidate distribution sets {}", distributionSetInvalidation.getDistributionSetIds());
final String tenant = tenantAware.getCurrentTenant(); final String tenant = tenantAware.getCurrentTenant();
if (shouldRolloutsBeCanceled(distributionSetInvalidation.getCancelationType(), distributionSetInvalidation.isCancelRollouts())) { if (shouldRolloutsBeCanceled(distributionSetInvalidation.getActionCancellationType())) {
final String handlerId = JpaRolloutManagement.createRolloutLockKey(tenant); final String handlerId = JpaRolloutManagement.createRolloutLockKey(tenant);
final Lock lock = lockRegistry.obtain(handlerId); final Lock lock = lockRegistry.obtain(handlerId);
try { try {
@@ -108,30 +108,30 @@ public class JpaDistributionSetInvalidationManagement implements DistributionSet
final DistributionSetInvalidation distributionSetInvalidation) { final DistributionSetInvalidation distributionSetInvalidation) {
return systemSecurityContext.runAsSystem(() -> { return systemSecurityContext.runAsSystem(() -> {
final Collection<Long> setIds = distributionSetInvalidation.getDistributionSetIds(); final Collection<Long> setIds = distributionSetInvalidation.getDistributionSetIds();
final long rolloutsCount = shouldRolloutsBeCanceled(distributionSetInvalidation.getCancelationType(), final long rolloutsCount = shouldRolloutsBeCanceled(distributionSetInvalidation.getActionCancellationType()) ? countRolloutsForInvalidation(setIds) : 0;
distributionSetInvalidation.isCancelRollouts()) ? countRolloutsForInvalidation(setIds) : 0;
final long autoAssignmentsCount = countAutoAssignmentsForInvalidation(setIds); final long autoAssignmentsCount = countAutoAssignmentsForInvalidation(setIds);
final long actionsCount = countActionsForInvalidation(setIds, final long actionsCount = countActionsForInvalidation(setIds,
distributionSetInvalidation.getCancelationType()); distributionSetInvalidation.getActionCancellationType());
return new DistributionSetInvalidationCount(rolloutsCount, autoAssignmentsCount, actionsCount); return new DistributionSetInvalidationCount(rolloutsCount, autoAssignmentsCount, actionsCount);
}); });
} }
private static boolean shouldRolloutsBeCanceled(final CancelationType cancelationType, private static boolean shouldRolloutsBeCanceled(final ActionCancellationType cancelationType) {
final boolean cancelRollouts) { return cancelationType != ActionCancellationType.NONE;
return cancelationType != CancelationType.NONE || cancelRollouts;
} }
private void invalidateDistributionSetsInTransaction(final DistributionSetInvalidation distributionSetInvalidation, final String tenant) { private void invalidateDistributionSetsInTransaction(final DistributionSetInvalidation distributionSetInvalidation,
final String tenant) {
DeploymentHelper.runInNewTransaction(txManager, tenant + "-invalidateDS", status -> { DeploymentHelper.runInNewTransaction(txManager, tenant + "-invalidateDS", status -> {
distributionSetInvalidation.getDistributionSetIds().forEach(setId -> invalidateDistributionSet( distributionSetInvalidation.getDistributionSetIds().forEach(setId -> invalidateDistributionSet(setId,
setId, distributionSetInvalidation.getCancelationType(), distributionSetInvalidation.isCancelRollouts())); distributionSetInvalidation.getActionCancellationType()));
return 0; 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); final DistributionSet distributionSet = distributionSetManagement.getOrElseThrowException(setId);
if (!distributionSet.isComplete()) { if (!distributionSet.isComplete()) {
throw new IncompleteDistributionSetException( throw new IncompleteDistributionSetException(
@@ -141,18 +141,18 @@ public class JpaDistributionSetInvalidationManagement implements DistributionSet
log.debug("Distribution set {} marked as invalid.", setId); log.debug("Distribution set {} marked as invalid.", setId);
// rollout cancellation should only be permitted with UPDATE_ROLLOUT permission // 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); log.debug("Cancel rollouts after ds invalidation. ID: {}", setId);
rolloutManagement.cancelRolloutsForDistributionSet(distributionSet); rolloutManagement.cancelRolloutsForDistributionSet(distributionSet, cancelationType);
} }
// Do run as system to ensure all actions (even invisible) are canceled due to invalidation. if (cancelationType != ActionCancellationType.NONE) {
systemSecurityContext.runAsSystem(() -> {
if (cancelationType != CancelationType.NONE) {
log.debug("Cancel actions after ds invalidation. ID: {}", setId); log.debug("Cancel actions after ds invalidation. ID: {}", setId);
deploymentManagement.cancelActionsForDistributionSet(cancelationType, distributionSet); deploymentManagement.cancelActionsForDistributionSet(cancelationType, distributionSet);
} }
// Do run as system to ensure all actions (even invisible) are canceled due to invalidation.
systemSecurityContext.runAsSystem(() -> {
log.debug("Cancel auto assignments after ds invalidation. ID: {}", setId); log.debug("Cancel auto assignments after ds invalidation. ID: {}", setId);
targetFilterQueryManagement.cancelAutoAssignmentForDistributionSet(setId); targetFilterQueryManagement.cancelAutoAssignmentForDistributionSet(setId);
return null; return null;
@@ -167,11 +167,11 @@ public class JpaDistributionSetInvalidationManagement implements DistributionSet
return setIds.stream().mapToLong(targetFilterQueryManagement::countByAutoAssignDistributionSetId).sum(); return setIds.stream().mapToLong(targetFilterQueryManagement::countByAutoAssignDistributionSetId).sum();
} }
private long countActionsForInvalidation(final Collection<Long> setIds, final CancelationType cancelationType) { private long countActionsForInvalidation(final Collection<Long> setIds, final ActionCancellationType cancelationType) {
long affectedActionsByDSInvalidation = 0; long affectedActionsByDSInvalidation = 0;
if (cancelationType == CancelationType.FORCE) { if (cancelationType == ActionCancellationType.FORCE) {
affectedActionsByDSInvalidation = countActionsForForcedInvalidation(setIds); affectedActionsByDSInvalidation = countActionsForForcedInvalidation(setIds);
} else if (cancelationType == CancelationType.SOFT) { } else if (cancelationType == ActionCancellationType.SOFT) {
affectedActionsByDSInvalidation = countActionsForSoftInvalidation(setIds); affectedActionsByDSInvalidation = countActionsForSoftInvalidation(setIds);
} }
return affectedActionsByDSInvalidation; return affectedActionsByDSInvalidation;

View File

@@ -19,6 +19,8 @@ import java.util.function.Function;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jakarta.validation.ConstraintDeclarationException; import jakarta.validation.ConstraintDeclarationException;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.ValidationException; 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.im.authentication.SpRole;
import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy; import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
import org.eclipse.hawkbit.repository.RolloutFields; 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.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException;
import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; 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.JpaManagementHelper;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaBaseEntity_; 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.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; import org.eclipse.hawkbit.repository.jpa.model.JpaRollout;
import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup;
import org.eclipse.hawkbit.repository.jpa.model.JpaRollout_; import org.eclipse.hawkbit.repository.jpa.model.JpaRollout_;
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository; 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.RolloutGroupRepository;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutRepository; 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.rollout.condition.StartNextGroupRolloutGroupSuccessAction;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility; 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.specifications.RolloutSpecification;
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper; 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.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.Rollout; 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.repository.model.TotalTargetCountStatus;
import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.utils.ObjectCopyUtil; 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.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.dao.ConcurrencyFailureException; import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page; 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.CREATING, RolloutStatus.READY, RolloutStatus.WAITING_FOR_APPROVAL, RolloutStatus.STARTING, RolloutStatus.RUNNING,
RolloutStatus.PAUSED, RolloutStatus.APPROVAL_DENIED); 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 RolloutRepository rolloutRepository;
private final RolloutGroupRepository rolloutGroupRepository; private final RolloutGroupRepository rolloutGroupRepository;
private final RolloutApprovalStrategy rolloutApprovalStrategy; private final RolloutApprovalStrategy rolloutApprovalStrategy;
@@ -113,42 +131,55 @@ public class JpaRolloutManagement implements RolloutManagement {
private final RolloutStatusCache rolloutStatusCache; private final RolloutStatusCache rolloutStatusCache;
private final ActionRepository actionRepository; private final ActionRepository actionRepository;
private final TargetManagement<? extends Target> targetManagement; private final TargetManagement<? extends Target> targetManagement;
private final ActionStatusRepository actionStatusRepository;
private final DistributionSetManagement<? extends DistributionSet> distributionSetManagement; private final DistributionSetManagement<? extends DistributionSet> distributionSetManagement;
private final TenantAware tenantAware;
private final TenantConfigurationManagement tenantConfigurationManagement; private final TenantConfigurationManagement tenantConfigurationManagement;
private final QuotaManagement quotaManagement; private final QuotaManagement quotaManagement;
private final AfterTransactionCommitExecutor afterCommit; private final AfterTransactionCommitExecutor afterCommit;
private final SystemSecurityContext systemSecurityContext; private final SystemSecurityContext systemSecurityContext;
private final ContextAware contextAware; private final ContextAware contextAware;
private final RepositoryProperties repositoryProperties; private final RepositoryProperties repositoryProperties;
private final OnlineDsAssignmentStrategy onlineDsAssignmentStrategy;
protected JpaRolloutManagement( protected JpaRolloutManagement(
final EntityManager entityManager,
final RolloutRepository rolloutRepository, final RolloutRepository rolloutRepository,
final RolloutGroupRepository rolloutGroupRepository, final RolloutGroupRepository rolloutGroupRepository,
final RolloutApprovalStrategy rolloutApprovalStrategy, final RolloutApprovalStrategy rolloutApprovalStrategy,
final StartNextGroupRolloutGroupSuccessAction startNextRolloutGroupAction, final StartNextGroupRolloutGroupSuccessAction startNextRolloutGroupAction,
final RolloutStatusCache rolloutStatusCache, final RolloutStatusCache rolloutStatusCache,
final ActionRepository actionRepository, final ActionRepository actionRepository,
final ActionStatusRepository actionStatusRepository,
final TargetRepository targetRepository,
final TargetManagement<? extends Target> targetManagement, final TargetManagement<? extends Target> targetManagement,
final DistributionSetManagement<? extends DistributionSet> distributionSetManagement, final DistributionSetManagement<? extends DistributionSet> distributionSetManagement,
final TenantAware tenantAware,
final TenantConfigurationManagement tenantConfigurationManagement, final TenantConfigurationManagement tenantConfigurationManagement,
final QuotaManagement quotaManagement, final QuotaManagement quotaManagement,
final AfterTransactionCommitExecutor afterCommit, final AfterTransactionCommitExecutor afterCommit,
final SystemSecurityContext systemSecurityContext, final ContextAware contextAware, final SystemSecurityContext systemSecurityContext, final ContextAware contextAware,
final RepositoryProperties repositoryProperties) { final RepositoryProperties repositoryProperties) {
this.entityManager = entityManager;
this.rolloutRepository = rolloutRepository; this.rolloutRepository = rolloutRepository;
this.rolloutGroupRepository = rolloutGroupRepository; this.rolloutGroupRepository = rolloutGroupRepository;
this.rolloutApprovalStrategy = rolloutApprovalStrategy; this.rolloutApprovalStrategy = rolloutApprovalStrategy;
this.startNextRolloutGroupAction = startNextRolloutGroupAction; this.startNextRolloutGroupAction = startNextRolloutGroupAction;
this.rolloutStatusCache = rolloutStatusCache; this.rolloutStatusCache = rolloutStatusCache;
this.actionRepository = actionRepository; this.actionRepository = actionRepository;
this.actionStatusRepository = actionStatusRepository;
this.targetManagement = targetManagement; this.targetManagement = targetManagement;
this.distributionSetManagement = distributionSetManagement; this.distributionSetManagement = distributionSetManagement;
this.tenantAware = tenantAware;
this.tenantConfigurationManagement = tenantConfigurationManagement; this.tenantConfigurationManagement = tenantConfigurationManagement;
this.quotaManagement = quotaManagement; this.quotaManagement = quotaManagement;
this.afterCommit = afterCommit; this.afterCommit = afterCommit;
this.systemSecurityContext = systemSecurityContext; this.systemSecurityContext = systemSecurityContext;
this.contextAware = contextAware; this.contextAware = contextAware;
this.repositoryProperties = repositoryProperties; this.repositoryProperties = repositoryProperties;
this.onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, actionRepository, actionStatusRepository,
quotaManagement, this::isMultiAssignmentsEnabled, this::isConfirmationFlowEnabled, repositoryProperties);
} }
public static String createRolloutLockKey(final String tenant) { public static String createRolloutLockKey(final String tenant) {
@@ -409,27 +440,49 @@ public class JpaRolloutManagement implements RolloutManagement {
@Transactional @Transactional
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, @Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
backoff = @Backoff(delay = Constants.TX_RT_DELAY)) backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public void delete(final long rolloutId) { public Rollout stop(long rolloutId) {
final JpaRollout jpaRollout = rolloutRepository.findById(rolloutId) final JpaRollout jpaRollout = rolloutRepository.findById(rolloutId)
.orElseThrow(() -> new EntityNotFoundException(Rollout.class, rolloutId)); .orElseThrow(() -> new EntityNotFoundException(Rollout.class, rolloutId));
if (RolloutStatus.DELETING == jpaRollout.getStatus()) { if (!ROLLOUT_STATUS_STOPPABLE.contains(jpaRollout.getStatus())) {
return; 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 @Override
@Transactional @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 // stop all rollouts for this distribution set
if (cancelationType.equals(ActionCancellationType.SOFT)) {
rolloutRepository.findByDistributionSetAndStatusIn(set, ROLLOUT_STATUS_STOPPABLE).forEach(rollout -> { rolloutRepository.findByDistributionSetAndStatusIn(set, ROLLOUT_STATUS_STOPPABLE).forEach(rollout -> {
final JpaRollout jpaRollout = (JpaRollout) rollout; final JpaRollout jpaRollout = (JpaRollout) rollout;
jpaRollout.setStatus(RolloutStatus.STOPPING); jpaRollout.setStatus(RolloutStatus.STOPPING);
rolloutRepository.save(jpaRollout); rolloutRepository.save(jpaRollout);
log.debug("Rollout {} stopped", jpaRollout.getId()); 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 @Override
@@ -459,6 +512,124 @@ public class JpaRolloutManagement implements RolloutManagement {
startNextRolloutGroupAction.exec(rollout, latestRunning); 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<JpaAction> 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<JpaAction> 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<Long> 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<JpaAction> actions, Action.Status status) {
final List<JpaActionStatus> 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<Long> 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<Long> 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<JpaAction> 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<Rollout> appendStatusDetails(final Page<Rollout> rollouts) { private Page<Rollout> appendStatusDetails(final Page<Rollout> rollouts) {
final List<Long> rolloutIds = rollouts.getContent().stream().map(Rollout::getId).toList(); final List<Long> rolloutIds = rollouts.getContent().stream().map(Rollout::getId).toList();
final Map<Long, List<TotalTargetCountActionStatus>> allStatesForRollout = getStatusCountItemForRollout(rolloutIds); final Map<Long, List<TotalTargetCountActionStatus>> allStatesForRollout = getStatusCountItemForRollout(rolloutIds);
@@ -851,5 +1022,13 @@ public class JpaRolloutManagement implements RolloutManagement {
return new TargetCount(totalTargets, baseFilter); 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) {} private record TargetCount(long total, String filter) {}
} }

View File

@@ -176,7 +176,7 @@ class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
} }
} }
void cancelAssignment(final JpaAction action) { void sendCancellationMessage(final JpaAction action) {
if (isMultiAssignmentsEnabled()) { if (isMultiAssignmentsEnabled()) {
sendMultiActionCancelEvent(action); sendMultiActionCancelEvent(action);
} else { } else {
@@ -184,6 +184,14 @@ class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
} }
} }
void sendCancellationMessages(final List<JpaAction> actions, final String tenant) {
if(isMultiAssignmentsEnabled()) {
sendMultiActionCancelEvent(tenant, Collections.unmodifiableList(actions));
} else {
actions.forEach(this::cancelAssignDistributionSetEvent);
}
}
private static Stream<Action> filterCancellations(final List<Action> actions) { private static Stream<Action> filterCancellations(final List<Action> actions) {
return actions.stream().filter(action -> { return actions.stream().filter(action -> {
final Status actionStatus = action.getStatus(); final Status actionStatus = action.getStatus();

View File

@@ -183,6 +183,25 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction> {
*/ */
Long countByRolloutIdAndStatus(Long rolloutId, Action.Status status); 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} * Returns {@code true} if actions for the given rollout exists, otherwise {@code false}
* <p/> * <p/>

View File

@@ -28,6 +28,8 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_;
import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action;
import org.springframework.data.jpa.domain.Specification; 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. * 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)); cb.equal(root.get(JpaAction_.active), true));
} }
public static Specification<JpaAction> 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<JpaAction> byRolloutIdAndActiveAndStatusIsNot(final Long rolloutId, final List<Action.Status> 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<JpaAction> byDistributionSetIdAndActiveAndStatusIsNot(final Long distributionSetId, final Action.Status status) { public static Specification<JpaAction> byDistributionSetIdAndActiveAndStatusIsNot(final Long distributionSetId, final Action.Status status) {
return (root, query, cb) -> cb.and( return (root, query, cb) -> cb.and(
cb.equal(root.get(JpaAction_.distributionSet).get(AbstractJpaBaseEntity_.id), distributionSetId), cb.equal(root.get(JpaAction_.distributionSet).get(AbstractJpaBaseEntity_.id), distributionSetId),

View File

@@ -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.model.JpaRolloutGroup;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository; import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository;
import org.eclipse.hawkbit.repository.model.Action.ActionType; 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.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; 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.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupErrorAction; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupErrorAction;
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupErrorCondition; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupErrorCondition;
@@ -78,7 +78,7 @@ class ConcurrentDistributionSetInvalidationTest extends AbstractJpaIntegrationTe
() -> rolloutGroupManagement.findByRollout(rollout.getId(), PAGE).getSize() > 0))); () -> rolloutGroupManagement.findByRollout(rollout.getId(), PAGE).getSize() > 0)));
final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
Collections.singletonList(distributionSet.getId()), CancelationType.SOFT, true); Collections.singletonList(distributionSet.getId()), ActionCancellationType.SOFT);
assertThatExceptionOfType(StopRolloutException.class) assertThatExceptionOfType(StopRolloutException.class)
.as("Invalidation of distributionSet should throw an exception") .as("Invalidation of distributionSet should throw an exception")
.isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation)); .isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation));

View File

@@ -17,8 +17,8 @@ import org.eclipse.hawkbit.im.authentication.SpRole;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.model.Action; 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.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@@ -294,7 +294,7 @@ class DeploymentManagementSecurityTest extends AbstractJpaIntegrationTest {
void cancelActionsForDistributionSetPermissionsCheck() { void cancelActionsForDistributionSetPermissionsCheck() {
assertPermissions(() -> { assertPermissions(() -> {
deploymentManagement.cancelActionsForDistributionSet( deploymentManagement.cancelActionsForDistributionSet(
DistributionSetInvalidation.CancelationType.FORCE, new JpaDistributionSet()); ActionCancellationType.FORCE, new JpaDistributionSet());
return null; return null;
}, List.of(SpPermission.UPDATE_TARGET)); }, List.of(SpPermission.UPDATE_TARGET));
} }

View File

@@ -15,6 +15,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; 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.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications; import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications;
import org.eclipse.hawkbit.repository.model.Action.Status; 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.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; 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.DistributionSetInvalidationCount;
import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus;
import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
@@ -43,6 +43,7 @@ import org.springframework.data.repository.query.Param;
* Feature: Component Tests - Repository<br/> * Feature: Component Tests - Repository<br/>
* Story: Distribution set invalidation management * Story: Distribution set invalidation management
*/ */
@Slf4j
class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTest { class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTest {
/** /**
@@ -53,8 +54,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe
final InvalidationTestData invalidationTestData = createInvalidationTestData("verifyInvalidateDistributionSetStopAutoAssignment"); final InvalidationTestData invalidationTestData = createInvalidationTestData("verifyInvalidateDistributionSetStopAutoAssignment");
final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE, Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.NONE);
false);
final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement
.countEntitiesForInvalidation(distributionSetInvalidation); .countEntitiesForInvalidation(distributionSetInvalidation);
assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 0, 0); 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 @Test
void verifyInvalidateDistributionSetStopRollouts() { void verifyInvalidateDistributionSetDoesNotStopRollouts() {
final InvalidationTestData invalidationTestData = createInvalidationTestData( final InvalidationTestData invalidationTestData = createInvalidationTestData(
"verifyInvalidateDistributionSetStopRollouts"); "verifyInvalidateDistributionSetStopRollouts");
final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE, Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.NONE);
true);
final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement
.countEntitiesForInvalidation(distributionSetInvalidation); .countEntitiesForInvalidation(distributionSetInvalidation);
assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 0, 1); assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 0, 0);
distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation); distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation);
rolloutHandler.handleAll(); rolloutHandler.handleAll();
@@ -96,9 +95,9 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe
assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get() assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get()
.getAutoAssignDistributionSet()).isNull(); .getAutoAssignDistributionSet()).isNull();
assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus()) assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus())
.isEqualTo(RolloutStatus.FINISHED); .isEqualTo(RolloutStatus.READY);
assertNoScheduledActionsExist(invalidationTestData.getRollout()); assertNoScheduledActionsExist(invalidationTestData.getRollout());
assertRolloutGroupsAreFinished(invalidationTestData.getRollout());
for (final Target target : invalidationTestData.getTargets()) { for (final Target target : invalidationTestData.getTargets()) {
// if status is pending, the assignment has not been canceled // if status is pending, the assignment has not been canceled
assertThat( assertThat(
@@ -118,8 +117,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe
"verifyInvalidateDistributionSetStopAllAndForceCancel"); "verifyInvalidateDistributionSetStopAllAndForceCancel");
final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.FORCE, Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.FORCE);
true);
final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement
.countEntitiesForInvalidation(distributionSetInvalidation); .countEntitiesForInvalidation(distributionSetInvalidation);
assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 5, 1); assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 5, 1);
@@ -129,16 +127,10 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe
assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get() assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get()
.getAutoAssignDistributionSet()).isNull(); .getAutoAssignDistributionSet()).isNull();
assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus()) // rollout should be deleted when force invalidation
.isEqualTo(RolloutStatus.FINISHED); assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId())).isEmpty();
assertNoScheduledActionsExist(invalidationTestData.getRollout()); assertNoScheduledActionsExist(invalidationTestData.getRollout());
assertRolloutGroupsAreFinished(invalidationTestData.getRollout()); assertAllRolloutActionsAreCancelled(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);
}
} }
/** /**
@@ -149,7 +141,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe
final InvalidationTestData invalidationTestData = createInvalidationTestData("verifyInvalidateDistributionSetStopAll"); final InvalidationTestData invalidationTestData = createInvalidationTestData("verifyInvalidateDistributionSetStopAll");
final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.SOFT,true); Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.SOFT);
final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement
.countEntitiesForInvalidation(distributionSetInvalidation); .countEntitiesForInvalidation(distributionSetInvalidation);
assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 5, 1); assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 5, 1);
@@ -176,7 +168,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe
final DistributionSet distributionSet = testdataFactory.createIncompleteDistributionSet(); final DistributionSet distributionSet = testdataFactory.createIncompleteDistributionSet();
final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
List.of(distributionSet.getId()), CancelationType.SOFT, true); List.of(distributionSet.getId()), ActionCancellationType.SOFT);
assertThatExceptionOfType(IncompleteDistributionSetException.class) assertThatExceptionOfType(IncompleteDistributionSetException.class)
.as("Incomplete distributionSet should throw an exception") .as("Incomplete distributionSet should throw an exception")
.isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation)); .isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation));
@@ -193,7 +185,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe
final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet(); final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet();
distributionSetInvalidationManagement.invalidateDistributionSet( distributionSetInvalidationManagement.invalidateDistributionSet(
new DistributionSetInvalidation(Collections.singletonList(distributionSet.getId()), new DistributionSetInvalidation(Collections.singletonList(distributionSet.getId()),
CancelationType.SOFT, true)); ActionCancellationType.SOFT));
} }
/** /**
@@ -206,8 +198,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe
.runAsSystem(() -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAuthority")); .runAsSystem(() -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAuthority"));
distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation( distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation(
Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE, Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.NONE));
false));
assertThat( assertThat(
distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid()) distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid())
.isFalse(); .isFalse();
@@ -223,14 +214,13 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe
() -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAndUpdateTargetAuthority")); () -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAndUpdateTargetAuthority"));
final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
List.of(invalidationTestData.getDistributionSet().getId()), CancelationType.SOFT, true); List.of(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.SOFT);
assertThatExceptionOfType(InsufficientPermissionException.class) assertThatExceptionOfType(InsufficientPermissionException.class)
.as("Insufficient permission exception expected") .as("Insufficient permission exception expected")
.isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation)); .isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation));
distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation( distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation(
Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE, Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.NONE));
false));
assertThat( assertThat(
distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid()) distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid())
.isFalse(); .isFalse();
@@ -246,8 +236,7 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe
() -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAndUpdateTargetAuthority")); () -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAndUpdateTargetAuthority"));
distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation( distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation(
Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.SOFT, Collections.singletonList(invalidationTestData.getDistributionSet().getId()), ActionCancellationType.SOFT));
true));
assertThat( assertThat(
distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid()) distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid())
.isFalse(); .isFalse();
@@ -259,10 +248,10 @@ class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTe
.isZero(); .isZero();
} }
private void assertRolloutGroupsAreFinished(final Rollout rollout) { private void assertAllRolloutActionsAreCancelled(final Rollout rollout) {
assertThat(rolloutGroupRepository.findByRolloutId(rollout.getId(), PAGE)) assertThat(
.isNotEmpty() actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), Status.CANCELED).getTotalElements())
.allMatch(rolloutGroup -> rolloutGroup.getStatus().equals(RolloutGroupStatus.FINISHED)); .isZero();
} }
private InvalidationTestData createInvalidationTestData(final String testName) { private InvalidationTestData createInvalidationTestData(final String testName) {

View File

@@ -50,11 +50,11 @@ import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThis
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.model.Action.Status; 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.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetFilter; import org.eclipse.hawkbit.repository.model.DistributionSetFilter;
import org.eclipse.hawkbit.repository.model.DistributionSetFilter.DistributionSetFilterBuilder; import org.eclipse.hawkbit.repository.model.DistributionSetFilter.DistributionSetFilterBuilder;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; 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.DistributionSetTag;
import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.NamedEntity; import org.eclipse.hawkbit.repository.model.NamedEntity;
@@ -482,7 +482,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
final Long softwareModuleId = testdataFactory.createSoftwareModuleOs().getId(); final Long softwareModuleId = testdataFactory.createSoftwareModuleOs().getId();
distributionSetManagement.assignSoftwareModules(distributionSetId, singletonList(softwareModuleId)); distributionSetManagement.assignSoftwareModules(distributionSetId, singletonList(softwareModuleId));
distributionSetInvalidationManagement.invalidateDistributionSet( distributionSetInvalidationManagement.invalidateDistributionSet(
new DistributionSetInvalidation(singletonList(distributionSetId), CancelationType.NONE, false)); new DistributionSetInvalidation(singletonList(distributionSetId), ActionCancellationType.NONE));
assertThatExceptionOfType(InvalidDistributionSetException.class) assertThatExceptionOfType(InvalidDistributionSetException.class)
.as("Invalid distributionSet should throw an exception").isThrownBy(() -> distributionSetManagement .as("Invalid distributionSet should throw an exception").isThrownBy(() -> distributionSetManagement
@@ -876,7 +876,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
distributionSetManagement.createMetadata(dsId, Map.of(knownKey1, knownValue)); distributionSetManagement.createMetadata(dsId, Map.of(knownKey1, knownValue));
distributionSetInvalidationManagement.invalidateDistributionSet( distributionSetInvalidationManagement.invalidateDistributionSet(
new DistributionSetInvalidation(singletonList(dsId), CancelationType.NONE, false)); new DistributionSetInvalidation(singletonList(dsId), ActionCancellationType.NONE));
// assert that no new metadata can be created // assert that no new metadata can be created
assertThatExceptionOfType(InvalidDistributionSetException.class) assertThatExceptionOfType(InvalidDistributionSetException.class)

View File

@@ -20,7 +20,9 @@ import org.eclipse.hawkbit.repository.RolloutManagement.Create;
import org.eclipse.hawkbit.repository.RolloutManagement.GroupCreate; import org.eclipse.hawkbit.repository.RolloutManagement.GroupCreate;
import org.eclipse.hawkbit.repository.RolloutManagement.Update; import org.eclipse.hawkbit.repository.RolloutManagement.Update;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; 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.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation;
import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder; import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder;
import org.eclipse.hawkbit.repository.test.util.TestdataFactory; 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(); DistributionSetManagement.Create.builder().type(defaultDsType()).name("name").version("1.0.0").build();
final DistributionSet ds = distributionSetManagement.create(dsCreate); final DistributionSet ds = distributionSetManagement.create(dsCreate);
assertPermissions(() -> { assertPermissions(() -> {
rolloutManagement.cancelRolloutsForDistributionSet(ds); rolloutManagement.cancelRolloutsForDistributionSet(ds, ActionCancellationType.SOFT);
return null; return null;
}, List.of(SpPermission.UPDATE_ROLLOUT, SpPermission.READ_REPOSITORY, SpPermission.CREATE_REPOSITORY)); }, List.of(SpPermission.UPDATE_ROLLOUT, SpPermission.READ_REPOSITORY, SpPermission.CREATE_REPOSITORY));
} }

View File

@@ -1914,11 +1914,11 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
@Expect(type = TargetUpdatedEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 2),
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
@Expect(type = ActionCreatedEvent.class, count = 10), @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 = RolloutCreatedEvent.class, count = 1),
@Expect(type = RolloutUpdatedEvent.class, count = 6), @Expect(type = RolloutUpdatedEvent.class, count = 6),
@Expect(type = RolloutDeletedEvent.class, count = 1), @Expect(type = RolloutDeletedEvent.class, count = 1),
@Expect(type = RolloutGroupUpdatedEvent.class, count = 11), @Expect(type = RolloutGroupUpdatedEvent.class, count = 16),
@Expect(type = RolloutGroupCreatedEvent.class, count = 5) }) @Expect(type = RolloutGroupCreatedEvent.class, count = 5) })
void deleteRolloutWhichHasBeenStartedBeforeIsSoftDeleted() { void deleteRolloutWhichHasBeenStartedBeforeIsSoftDeleted() {
final int amountTargetsForRollout = 10; final int amountTargetsForRollout = 10;
@@ -1962,8 +1962,8 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
// verify that all scheduled actions are deleted // verify that all scheduled actions are deleted
assertThat(actionRepository.findByRolloutIdAndStatus(PAGE, deletedRollout.getId(), Status.SCHEDULED) assertThat(actionRepository.findByRolloutIdAndStatus(PAGE, deletedRollout.getId(), Status.SCHEDULED)
.getNumberOfElements()).isZero(); .getNumberOfElements()).isZero();
// verify that all running actions keep running // verify that all running actions are force cancelled
assertThat(actionRepository.findByRolloutIdAndStatus(PAGE, deletedRollout.getId(), Status.RUNNING) assertThat(actionRepository.findByRolloutIdAndStatus(PAGE, deletedRollout.getId(), Status.CANCELED)
.getNumberOfElements()).isEqualTo(2); .getNumberOfElements()).isEqualTo(2);
} }

View File

@@ -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.ActionStatusCreate;
import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.Action.Status; 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.ActionStatus;
import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.Artifact;
import org.eclipse.hawkbit.repository.model.ArtifactUpload; 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.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; 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.DistributionSetTag;
import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.NamedEntity; import org.eclipse.hawkbit.repository.model.NamedEntity;
@@ -1082,6 +1082,19 @@ public class TestdataFactory {
createDistributionSet(prefix), "50", "5"); 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 * Create the data for a simple rollout scenario
* *
@@ -1212,7 +1225,7 @@ public class TestdataFactory {
public DistributionSet createAndInvalidateDistributionSet() { public DistributionSet createAndInvalidateDistributionSet() {
final DistributionSet distributionSet = createDistributionSet(); final DistributionSet distributionSet = createDistributionSet();
distributionSetInvalidationManagement.invalidateDistributionSet( 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(); return distributionSetManagement.get(distributionSet.getId()).orElseThrow();
} }