diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java index 9840f4dab..0d8888deb 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java @@ -32,7 +32,7 @@ public enum SpServerError { "The given entity cannot be saved due to Constraint Violation"), /** - * + * */ SP_REPO_INVALID_TARGET_ADDRESS("hawkbit.server.error.repo.invalidTargetAddress", "The target address is not well formed"), @@ -180,6 +180,11 @@ public enum SpServerError { SP_DS_INCOMPLETE("hawkbit.server.error.distributionset.incomplete", "Distribution set is assigned to a target that is incomplete (i.e. mandatory modules are missing)"), + /** + * + */ + SP_DS_INVALID("hawkbit.server.error.distributionset.invalid", "Invalid distribution set is assigned to a target"), + /** * */ @@ -210,18 +215,18 @@ public enum SpServerError { "The given entity is read only and the change cannot be completed."), /** - * + * */ SP_CONFIGURATION_VALUE_INVALID("hawkbit.server.error.configValueInvalid", "The given configuration value is invalid."), /** - * + * */ SP_CONFIGURATION_KEY_INVALID("hawkbit.server.error.configKeyInvalid", "The given configuration key is invalid."), /** - * + * */ SP_ROLLOUT_ILLEGAL_STATE("hawkbit.server.error.rollout.illegalstate", "The rollout is in the wrong state for the requested operation"), @@ -251,13 +256,6 @@ public enum SpServerError { SP_AUTO_ASSIGN_ACTION_TYPE_INVALID("hawkbit.server.error.repo.invalidAutoAssignActionType", "The given action type for auto-assignment is invalid: allowed values are ['forced', 'soft', 'downloadonly']"), - /** - * Error message informing that the distribution set for auto-assignment is - * invalid. - */ - SP_AUTO_ASSIGN_DISTRIBUTION_SET_INVALID("hawkbit.server.error.repo.invalidAutoAssignDistributionSet", - "The given distribution set for auto-assignment is invalid: it is either incomplete (i.e. mandatory modules are missing) or soft deleted"), - /** * Error message informing the user that the requested tenant configuration * change is not allowed. @@ -277,7 +275,9 @@ public enum SpServerError { SP_NO_WEIGHT_PROVIDED_IN_MULTIASSIGNMENT_MODE("hawkbit.server.error.noWeightProvidedInMultiAssignmentMode", "The requested operation requires a weight to be specified when multi assignments is enabled."), - SP_TARGET_TYPE_IN_USE("hawkbit.server.error.target.type.used", "Target type is still in use by a target."); + SP_TARGET_TYPE_IN_USE("hawkbit.server.error.target.type.used", "Target type is still in use by a target."), + + SP_STOP_ROLLOUT_FAILED("hawkbit.server.error.stopRolloutFailed", "Stopping the rollout failed"); private final String key; private final String message; @@ -292,7 +292,7 @@ public enum SpServerError { /** * Gets the key of the error - * + * * @return the key of the error */ public String getKey() { @@ -301,7 +301,7 @@ public enum SpServerError { /** * Gets the message of the error - * + * * @return message of the error */ public String getMessage() { diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java index f863a613c..f84cb5dbe 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java @@ -20,11 +20,11 @@ import javax.validation.constraints.NotNull; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; +import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; -import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.model.Action; @@ -34,6 +34,7 @@ import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.Target; @@ -56,7 +57,7 @@ public interface DeploymentManagement { * * @param deploymentRequests * information about all target-ds-assignments that shall be made - * + * * @return the list of assignment results * * @throws IncompleteDistributionSetException @@ -66,17 +67,18 @@ public interface DeploymentManagement { * @throws EntityNotFoundException * if either provided {@link DistributionSet} or {@link Target}s * do not exist - * + * * @throws AssignmentQuotaExceededException * if the maximum number of targets the distribution set can be * assigned to at once is exceeded * @throws MultiAssignmentIsNotEnabledException * if the request results in multiple assignments to the same * target and multiassignment is disabled - * + * */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) - List assignDistributionSets(@Valid @NotEmpty List deploymentRequests); + List assignDistributionSets( + @Valid @NotEmpty List deploymentRequests); /** * Assigns {@link DistributionSet}s to {@link Target}s according to the @@ -88,7 +90,7 @@ public interface DeploymentManagement { * information about all target-ds-assignments that shall be made * @param actionMessage * an optional message for the action status - * + * * @return the list of assignment results * * @throws IncompleteDistributionSetException @@ -98,23 +100,23 @@ public interface DeploymentManagement { * @throws EntityNotFoundException * if either provided {@link DistributionSet} or {@link Target}s * do not exist - * + * * @throws AssignmentQuotaExceededException * if the maximum number of targets the distribution set can be * assigned to at once is exceeded * @throws MultiAssignmentIsNotEnabledException * if the request results in multiple assignments to the same * target and multiassignment is disabled - * + * */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) List assignDistributionSets(String initiatedBy, @Valid @NotEmpty List deploymentRequests, String actionMessage); - - /** + + /** * build a {@link DeploymentRequest} for a target distribution set * assignment - * + * * @param controllerId * ID of target * @param distributionSetId @@ -128,9 +130,9 @@ public interface DeploymentManagement { /** * Registers "offline" assignments. "offline" assignment means adding a * completed action for a {@link DistributionSet} to a {@link Target}. - * + * * The handling differs to hawkBit-managed updates by means that:
- * + * *
    *
  1. it ignores targets completely that are in * {@link TargetUpdateStatus#PENDING}.
  2. @@ -139,12 +141,12 @@ public interface DeploymentManagement { * status to {@link TargetUpdateStatus#IN_SYNC}. *
  3. does not send a {@link TargetAssignDistributionSetEvent}.
  4. *
- * + * * @param assignments * target IDs with the respective distribution set ID which they * are supposed to be assigned to * @return the assignment results - * + * * @throws IncompleteDistributionSetException * if mandatory {@link SoftwareModuleType} are not assigned as * defined by the {@link DistributionSetType}. @@ -152,11 +154,11 @@ public interface DeploymentManagement { * @throws EntityNotFoundException * if either provided {@link DistributionSet} or {@link Target}s * do not exist - * + * * @throws AssignmentQuotaExceededException * if the maximum number of targets the distribution set can be * assigned to at once is exceeded - * + * * @throws MultiAssignmentIsNotEnabledException * if the request results in multiple assignments to the same * target and multiassignment is disabled @@ -173,7 +175,7 @@ public interface DeploymentManagement { * to be canceled * * @return canceled {@link Action} - * + * * @throws CancelActionNotAllowedException * in case the given action is not active or is already a cancel * action @@ -221,20 +223,43 @@ public interface DeploymentManagement { * @param controllerId * the target associated to the actions to count * @return the count value of found actions associated to the target - * + * * @throws EntityNotFoundException * if target with given ID does not exist */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) long countActionsByTarget(@NotEmpty String controllerId); + /** + * Counts all active {@link Action}s referring to the given DistributionSet. + * + * @param distributionSet + * DistributionSet to count the {@link Action}s from + * @return the count of actions referring to the given distributionSet + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + long countActionsByDistributionSetIdAndActiveIsTrue(Long distributionSet); + + /** + * Counts all active {@link Action}s referring to the given DistributionSet + * that are not in a given state. + * + * @param distributionSet + * DistributionSet to count the {@link Action}s from + * @param status + * the state the actions should not have + * @return the count of actions referring to the given distributionSet + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + long countActionsByDistributionSetIdAndActiveIsTrueAndStatusIsNot(Long distributionSet, Status status); + /** * Get the {@link Action} entity for given actionId. * * @param actionId * to be id of the action * @return the corresponding {@link Action} - * + * */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) Optional findAction(long actionId); @@ -252,7 +277,7 @@ public interface DeploymentManagement { /** * Retrieves all {@link Action} which assigned to a specific * {@link DistributionSet}. - * + * * @param pageable * the page request parameter for paging and sorting the result * @param distributionSetId @@ -260,7 +285,7 @@ public interface DeploymentManagement { * in the result * @return a list of {@link Action} which are assigned to a specific * {@link DistributionSet} - * + * * @throws EntityNotFoundException * if distribution set with given ID does not exist */ @@ -279,7 +304,7 @@ public interface DeploymentManagement { * the page request * @return a slice of actions assigned to the specific target and the * specification - * + * * @throws RSQLParameterUnsupportedFieldException * if a field in the RSQL string is used but not provided by the * given {@code fieldNameProvider} @@ -299,7 +324,7 @@ public interface DeploymentManagement { * @param pageable * the pageable request to limit, sort the actions * @return a slice of actions found for a specific target - * + * */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) Slice findActionsByTarget(@NotEmpty String controllerId, @NotNull Pageable pageable); @@ -313,7 +338,7 @@ public interface DeploymentManagement { * @param actionId * to be filtered on * @return the corresponding {@link Page} of {@link ActionStatus} - * + * * @throws EntityNotFoundException * if action with given ID does not exist */ @@ -346,13 +371,13 @@ public interface DeploymentManagement { /** * Retrieves all active {@link Action}s of a specific target. - * + * * @param pageable * the page request parameter for paging and sorting the result * @param controllerId * the target associated with the actions * @return a list of actions associated with the given target - * + * * @throws EntityNotFoundException * if target with given ID does not exist */ @@ -367,7 +392,7 @@ public interface DeploymentManagement { * @param controllerId * the target associated with the actions * @return a list of actions associated with the given target - * + * * @throws EntityNotFoundException * if target with given ID does not exist */ @@ -377,13 +402,13 @@ public interface DeploymentManagement { /** * Retrieves active {@link Action}s with highest weight that are assigned to * a {@link Target}. - * + * * @param controllerId * identifies the target to retrieve the action from * @param maxActionCount * max size of returned list * @return the action - * + * */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) List findActiveActionsWithHighestWeight(@NotEmpty String controllerId, int maxActionCount); @@ -391,7 +416,7 @@ public interface DeploymentManagement { /** * Get weight of an Action. Returns the default value if the weight is null * according to the properties. - * + * * @param action * to extract the weight from * @return weight of the action @@ -408,10 +433,10 @@ public interface DeploymentManagement { * to be canceled * * @return quite {@link Action} - * + * * @throws CancelActionNotAllowedException * in case the given action is not active - * + * * @throws EntityNotFoundException * if action with given ID does not exist */ @@ -425,7 +450,7 @@ public interface DeploymentManagement { * @param actionId * the ID of the action * @return the updated or the found {@link Action} - * + * * @throws EntityNotFoundException * if action with given ID does not exist */ @@ -438,7 +463,7 @@ public interface DeploymentManagement { * * @param targetIds * ids of the {@link Target}s the actions belong to - * + * */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) void cancelInactiveScheduledActionsForTargets(List targetIds); @@ -468,11 +493,11 @@ public interface DeploymentManagement { /** * Returns {@link DistributionSet} that is assigned to given {@link Target}. - * + * * @param controllerId * of target * @return assigned {@link DistributionSet} - * + * * @throws EntityNotFoundException * if target with given ID does not exist */ @@ -481,11 +506,11 @@ public interface DeploymentManagement { /** * Returns {@link DistributionSet} that is installed on given * {@link Target}. - * + * * @param controllerId * of target * @return installed {@link DistributionSet} - * + * * @throws EntityNotFoundException * if target with given ID does not exist */ @@ -494,20 +519,20 @@ public interface DeploymentManagement { /** * Deletes actions which match one of the given action status and which have * not been modified since the given (absolute) time-stamp. - * + * * @param status * Set of action status. * @param lastModified * A time-stamp in milliseconds. - * + * * @return The number of action entries that were deleted. */ @PreAuthorize(SpringEvalExpressions.IS_SYSTEM_CODE) int deleteActionsByStatusAndLastModifiedBefore(@NotNull Set status, long lastModified); /** - * Checks if there is an action for the device with the given controller ID that - * is in the {@link Action.Status#CANCELING} state. + * Checks if there is an action for the device with the given controller ID + * that is in the {@link Action.Status#CANCELING} state. * * @param controllerId * of target @@ -516,4 +541,16 @@ public interface DeploymentManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) boolean hasPendingCancellations(@NotEmpty String controllerId); + /** + * Cancels all actions that refer to a given distribution set. This method + * is called when a distribution set is invalidated. + * + * @param cancelationType + * defines if a force or soft cancel is executed + * @param set + * the distribution set for that the actions should be canceled + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + void cancelActionsForDistributionSet(final CancelationType cancelationType, final DistributionSet set); + } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetInvalidationManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetInvalidationManagement.java new file mode 100644 index 000000000..2b5b9a8da --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetInvalidationManagement.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidationCount; + +/** + * A DistributionSetInvalidationManagement service provides operations to + * invalidate {@link DistributionSet}s. + * + */ +public interface DistributionSetInvalidationManagement { + + /** + * Invalidates a given {@link DistributionSet}. The invalidation always + * cancels all auto assignments referring this {@link DistributionSet} and + * can not be undone. Optionally, all rollouts and actions referring this + * {@link DistributionSet} can be canceled. + * + * @param distributionSetInvalidation + * defines the {@link DistributionSet} and options what should be + * canceled + */ + void invalidateDistributionSet(final DistributionSetInvalidation distributionSetInvalidation); + + /** + * Counts all entities for a list of {@link DistributionSet}s that will be + * canceled when invalidation is called for those {@link DistributionSet}s. + * + * + * @param distributionSetInvalidation + * defines the {@link DistributionSet} and options what should be + * canceled + * @return The {@link DistributionSetInvalidationCount} object that holds + * information about the count of affected rollouts, + * auto-assignments and actions + */ + DistributionSetInvalidationCount countEntitiesForInvalidation( + final DistributionSetInvalidation distributionSetInvalidation); + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java index ff22b9f4c..4d908d0c7 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java @@ -18,10 +18,12 @@ import javax.validation.constraints.NotNull; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.repository.builder.DistributionSetCreate; import org.eclipse.hawkbit.repository.builder.DistributionSetUpdate; +import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; -import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; +import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThisDistributionSetException; @@ -53,20 +55,20 @@ public interface DistributionSetManagement * to assign and update * @param moduleIds * to get assigned - * + * * @return the updated {@link DistributionSet}. - * + * * @throws EntityNotFoundException * if (at least one) given module does not exist - * + * * @throws EntityReadOnlyException * if use tries to change the {@link DistributionSet} s while * the DS is already in use. - * + * * @throws UnsupportedSoftwareModuleForThisDistributionSetException * if {@link SoftwareModule#getType()} is not supported by this * {@link DistributionSet#getType()}. - * + * * @throws AssignmentQuotaExceededException * if the maximum number of {@link SoftwareModule}s is exceeded * for the addressed {@link DistributionSet}. @@ -82,9 +84,9 @@ public interface DistributionSetManagement * to assign for * @param tagId * to assign - * + * * @return list of assigned ds - * + * * @throws EntityNotFoundException * if tag with given ID does not exist or (at least one) of the * distribution sets. @@ -101,14 +103,14 @@ public interface DistributionSetManagement * @param metadata * the meta data entries to create or update * @return the updated or created distribution set meta data entries - * + * * @throws EntityNotFoundException * if given set does not exist - * + * * @throws EntityAlreadyExistsException * in case one of the meta data entry already exists for the * specific key - * + * * @throws AssignmentQuotaExceededException * if the maximum number of {@link MetaData} entries is exceeded * for the addressed {@link DistributionSet} @@ -123,7 +125,7 @@ public interface DistributionSetManagement * where meta data has to be deleted * @param key * of the meta data element - * + * * @throws EntityNotFoundException * if given set does not exist */ @@ -136,7 +138,7 @@ public interface DistributionSetManagement * @param actionId * the action associated with the distribution set * @return the distribution set which is associated with the action - * + * * @throws EntityNotFoundException * if action with given ID does not exist */ @@ -152,7 +154,7 @@ public interface DistributionSetManagement * * @param setId * to look for. - * + * * @return {@link DistributionSet} */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) @@ -165,15 +167,69 @@ public interface DistributionSetManagement * name of {@link DistributionSet}; case insensitive * @param version * version of {@link DistributionSet} - * + * * @return the page with the found {@link DistributionSet} */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) Optional getByNameAndVersion(@NotEmpty String distributionName, @NotEmpty String version); + /** + * Find distribution set by id and throw an exception if it is deleted, + * incomplete or invalidated. + * + * @param id + * id of {@link DistributionSet} + * + * @return the found valid {@link DistributionSet} + * + * @throws EntityNotFoundException + * if distribution set with given ID does not exist + * + * @throws InvalidDistributionSetException + * if distribution set with given ID is invalidated + * + * @throws IncompleteDistributionSetException + * if distribution set with given ID is incomplete + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + DistributionSet getValidAndComplete(long id); + + /** + * Find distribution set by id and throw an exception if it is deleted or + * invalidated. + * + * @param id + * id of {@link DistributionSet} + * + * @return the found valid {@link DistributionSet} + * + * @throws EntityNotFoundException + * if distribution set with given ID does not exist + * + * @throws InvalidDistributionSetException + * if distribution set with given ID is invalidated + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + DistributionSet getValid(long id); + + /** + * Find distribution set by id and throw an exception if it is (soft) + * deleted. + * + * @param id + * id of {@link DistributionSet} + * + * @return the found valid {@link DistributionSet} + * + * @throws EntityNotFoundException + * if distribution set with given ID does not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + DistributionSet getOrElseThrowException(long id); + /** * Finds all meta data by the given distribution set id. - * + * * @param pageable * the page request to page the result * @param setId @@ -181,7 +237,7 @@ public interface DistributionSetManagement * * @return a paged result of all meta data entries for a given distribution * set id - * + * * @throws EntityNotFoundException * if distribution set with given ID does not exist */ @@ -190,7 +246,7 @@ public interface DistributionSetManagement /** * Finds all meta data by the given distribution set id. - * + * * @param pageable * the page request to page the result * @param setId @@ -200,14 +256,14 @@ public interface DistributionSetManagement * * @return a paged result of all meta data entries for a given distribution * set id - * + * * @throws RSQLParameterUnsupportedFieldException * if a field in the RSQL string is used but not provided by the * given {@code fieldNameProvider} - * + * * @throws RSQLParameterSyntaxException * if the RSQL syntax is wrong - * + * * @throws EntityNotFoundException * of distribution set with given ID does not exist */ @@ -249,7 +305,7 @@ public interface DistributionSetManagement * has details of filters to be applied * @param assignedOrInstalled * the id of the Target to be ordered by - * + * * @return {@link DistributionSet}s */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) @@ -277,14 +333,14 @@ public interface DistributionSetManagement * @param tagId * of the tag the DS are assigned to * @return the page of found {@link DistributionSet} - * + * * @throws RSQLParameterUnsupportedFieldException * if a field in the RSQL string is used but not provided by the * given {@code fieldNameProvider} - * + * * @throws RSQLParameterSyntaxException * if the RSQL syntax is wrong - * + * * @throws EntityNotFoundException * of distribution set tag with given ID does not exist */ @@ -301,7 +357,7 @@ public interface DistributionSetManagement * @param tagId * of the tag the DS are assigned to * @return the page of found {@link DistributionSet} - * + * * @throws EntityNotFoundException * of distribution set tag with given ID does not exist */ @@ -316,7 +372,7 @@ public interface DistributionSetManagement * @param key * of the meta data element * @return the found DistributionSetMetadata - * + * * @throws EntityNotFoundException * is set with given ID does not exist */ @@ -329,7 +385,7 @@ public interface DistributionSetManagement * * @param setId * to check - * + * * @return true if in use */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) @@ -338,8 +394,8 @@ public interface DistributionSetManagement /** * Toggles {@link DistributionSetTag} assignment to given * {@link DistributionSet}s by means that if some (or all) of the targets in - * the list have the {@link Tag} not yet assigned, they will be. Only if all of - * theme have the tag already assigned they will be removed instead. + * the list have the {@link Tag} not yet assigned, they will be. Only if all + * of theme have the tag already assigned they will be removed instead. * * @param setIds * to toggle for @@ -347,7 +403,7 @@ public interface DistributionSetManagement * to toggle * @return {@link DistributionSetTagAssignmentResult} with all meta data of * the assignment outcome. - * + * * @throws EntityNotFoundException * if given tag does not exist or (at least one) module */ @@ -363,10 +419,10 @@ public interface DistributionSetManagement * @param moduleId * to be unassigned * @return the updated {@link DistributionSet}. - * + * * @throws EntityNotFoundException * if given module or DS does not exist - * + * * @throws EntityReadOnlyException * if use tries to change the {@link DistributionSet} s while * the DS is already in use. @@ -383,7 +439,7 @@ public interface DistributionSetManagement * @param tagId * to unassign * @return the unassigned ds or if no ds is unassigned - * + * * @throws EntityNotFoundException * if set or tag with given ID does not exist */ @@ -398,7 +454,7 @@ public interface DistributionSetManagement * @param metadata * meta data entry to be updated * @return the updated meta data entry - * + * * @throws EntityNotFoundException * in case the meta data entry does not exists and cannot be * updated @@ -409,16 +465,25 @@ public interface DistributionSetManagement /** * Count all {@link DistributionSet}s in the repository that are not marked * as deleted. - * + * * @param typeId * to look for * * @return number of {@link DistributionSet}s - * + * * @throws EntityNotFoundException * if type with given ID does not exist */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) long countByTypeId(long typeId); + /** + * Sets the specified {@link DistributionSet} as invalidated. + * + * @param set + * the ID of the {@link DistributionSet} to be set to invalid + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + void invalidate(DistributionSet set); + } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryProperties.java index 771707554..84cde3ad4 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryProperties.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryProperties.java @@ -59,6 +59,12 @@ public class RepositoryProperties { */ private int actionWeightIfAbsent = 1000; + /** + * Defines a timeout for the lock during invalidation of distribution sets + * (in seconds). + */ + private long dsInvalidationLockTimeout = 5; + public boolean isEagerPollPersistence() { return eagerPollPersistence; } @@ -107,4 +113,12 @@ public class RepositoryProperties { this.actionWeightIfAbsent = actionWeightIfAbsent; } + public long getDsInvalidationLockTimeout() { + return dsInvalidationLockTimeout; + } + + public void setDsInvalidationLockTimeout(final long dsInvalidationLockTimeout) { + this.dsInvalidationLockTimeout = dsInvalidationLockTimeout; + } + } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java index 393bc0ca1..f42abb3ed 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java @@ -20,9 +20,9 @@ import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.repository.builder.RolloutCreate; import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; import org.eclipse.hawkbit.repository.builder.RolloutUpdate; +import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; -import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; @@ -49,29 +49,29 @@ public interface RolloutManagement { /** * Process rollout based on its current {@link Rollout#getStatus()}. - * + * * For {@link RolloutStatus#CREATING} that means creating the * {@link RolloutGroup}s with {@link Target}s and when finished switch to * {@link RolloutStatus#READY}. - * + * * For {@link RolloutStatus#READY} that means switching to * {@link RolloutStatus#STARTING} if the {@link Rollout#getStartAt()} is set * and time of calling this method is beyond this point in time. This auto * start mechanism is optional. Call {@link #start(Long)} otherwise. - * + * * For {@link RolloutStatus#STARTING} that means starting the first * {@link RolloutGroup}s in line and when finished switch to * {@link RolloutStatus#RUNNING}. - * + * * For {@link RolloutStatus#RUNNING} that means checking to activate further * groups based on the defined thresholds. Switched to * {@link RolloutStatus#FINISHED} is all groups are finished. - * + * * For {@link RolloutStatus#DELETING} that means either soft delete in case * rollout was already {@link RolloutStatus#RUNNING} which results in status * change {@link RolloutStatus#DELETED} or hard delete from the persistence * otherwise. - * + * */ @PreAuthorize(SpringEvalExpressions.IS_SYSTEM_CODE) void handleRollouts(); @@ -95,6 +95,17 @@ public interface RolloutManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ) long countByFilters(@NotEmpty String searchText); + /** + * Counts all {@link Rollout}s for a specific {@link DistributionSet} that + * are stoppable + * + * @param setId + * the distribution set + * @return the count + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ) + long countByDistributionSetIdAndRolloutIsStoppable(long setId); + /** * Persists a new rollout entity. The filter within the * {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets @@ -217,7 +228,7 @@ public interface RolloutManagement { /** * Retrieves all rollouts found by the given specification. - * + * * @param pageable * the page request to sort and limit the result * @param rsqlParam @@ -226,7 +237,7 @@ public interface RolloutManagement { * flag if deleted rollouts should be included * * @return a page of found rollouts - * + * * @throws RSQLParameterUnsupportedFieldException * if a field in the RSQL string is used but not provided by the * given {@code fieldNameProvider} @@ -282,7 +293,7 @@ public interface RolloutManagement { * @param deleted * flag if deleted rollouts should be included * @return rollout details of targets count for different statuses - * + * * */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ) @@ -293,7 +304,7 @@ public interface RolloutManagement { * * @param rolloutId * rollout id - * + * * @return true if rollout exists */ boolean exists(long rolloutId); @@ -318,7 +329,7 @@ public interface RolloutManagement { * @throws RolloutIllegalStateException * if given rollout is not in {@link RolloutStatus#RUNNING}. * Only running rollouts can be paused. - * + * */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_HANDLE) void pauseRollout(long rolloutId); @@ -330,7 +341,7 @@ public interface RolloutManagement { * * @param rolloutId * the rollout to be resumed - * + * * @throws EntityNotFoundException * if rollout with given ID does not exist * @throws RolloutIllegalStateException @@ -368,14 +379,14 @@ public interface RolloutManagement { * {@link RolloutStatus#WAITING_FOR_APPROVAL}. If the rollout is approved, * it switches state to {@link RolloutStatus#READY}, otherwise it switches * to state {@link RolloutStatus#APPROVAL_DENIED} - * + * * @param rolloutId * the rollout to be approved or denied. * @param decision * decision whether a rollout is approved or denied. * @param remark * user remark on approve / deny decision - * + * * @return approved or denied rollout * * @throws EntityNotFoundException @@ -399,7 +410,7 @@ public interface RolloutManagement { * the rollout to be started * * @return started rollout - * + * * @throws EntityNotFoundException * if rollout with given ID does not exist * @throws RolloutIllegalStateException @@ -416,13 +427,13 @@ public interface RolloutManagement { * rollout to be updated * * @return Rollout updated rollout - * + * * @throws EntityNotFoundException * if rollout or DS with given IDs do not exist * @throws EntityReadOnlyException * if rollout is in soft deleted state, i.e. only kept as * reference - * + * */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_UPDATE) Rollout update(@NotNull @Valid RolloutUpdate update); @@ -430,12 +441,24 @@ public interface RolloutManagement { /** * Deletes a rollout. A rollout might be deleted asynchronously by * indicating the rollout by {@link RolloutStatus#DELETING} - * - * + * + * * @param rolloutId * the ID of the rollout to be deleted */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_DELETE) void delete(long rolloutId); + /** + * Cancels all rollouts that refer to the given {@link DistributionSet}. + * This is called when a distribution set is invalidated and the cancel + * rollouts option is activated. + * + * @param set + * the {@link DistributionSet} for that the rollouts should be + * canceled + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_UPDATE) + void cancelRolloutsForDistributionSet(DistributionSet set); + } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java index fd5e2fb42..615de1268 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java @@ -18,10 +18,11 @@ import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.repository.builder.AutoAssignDistributionSetUpdate; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryCreate; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryUpdate; -import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; -import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException; -import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignDistributionSetException; import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException; +import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.model.Action.ActionType; @@ -42,13 +43,13 @@ public interface TargetFilterQueryManagement { * * @param create * to create - * + * * @return the new {@link TargetFilterQuery} - * + * * @throws ConstraintViolationException * if fields are not filled as specified. Check * {@link TargetFilterQueryCreate} for field constraints. - * + * * @throws AssignmentQuotaExceededException * if the maximum number of targets that is addressed by the * given query is exceeded (auto-assignments only) @@ -61,7 +62,7 @@ public interface TargetFilterQueryManagement { * * @param targetFilterQueryId * IDs of target filter query to be deleted - * + * * @throws EntityNotFoundException * if filter with given ID does not exist */ @@ -70,16 +71,16 @@ public interface TargetFilterQueryManagement { /** * Verifies the provided filter syntax. - * + * * @param query * to verify - * + * * @return true if syntax is valid - * + * * @throws RSQLParameterUnsupportedFieldException * if a field in the RSQL string is used but not provided by the * given {@code fieldNameProvider} - * + * * @throws RSQLParameterSyntaxException * if the RSQL syntax is wrong */ @@ -99,12 +100,23 @@ public interface TargetFilterQueryManagement { /** * Counts all {@link TargetFilterQuery}s. - * + * * @return the number of all target filter queries */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) long count(); + /** + * Counts all target filters that have a given auto assign distribution set + * assigned. + * + * @param autoAssignDistributionSetId + * the id of the distribution set + * @return the count + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + long countByAutoAssignDistributionSetId(long autoAssignDistributionSetId); + /** * Retrieves all {@link TargetFilterQuery}s which match the given name * filter. @@ -154,7 +166,7 @@ public interface TargetFilterQueryManagement { * @param rsqlParam * RSQL filter * @return the page with the found {@link TargetFilterQuery}s - * + * * @throws EntityNotFoundException * if DS with given ID does not exist */ @@ -198,17 +210,17 @@ public interface TargetFilterQueryManagement { * * @param update * to be updated - * + * * @return the updated {@link TargetFilterQuery} - * + * * @throws EntityNotFoundException * if either {@link TargetFilterQuery} and/or autoAssignDs are * provided but not found - * + * * @throws ConstraintViolationException * if fields are not filled as specified. Check * {@link TargetFilterQueryUpdate} for field constraints. - * + * * @throws QuotaExceededException * if the update contains a new query which addresses too many * targets (auto-assignments only) @@ -218,29 +230,43 @@ public interface TargetFilterQueryManagement { /** * Updates the auto assign settings of an {@link TargetFilterQuery}. - * + * * @param autoAssignDistributionSetUpdate * the new auto assignment - * + * * @return the updated {@link TargetFilterQuery} - * + * * @throws EntityNotFoundException * if either {@link TargetFilterQuery} and/or autoAssignDs are * provided but not found - * + * * @throws AssignmentQuotaExceededException * if the query that is already associated with this filter * query addresses too many targets (auto-assignments only) - * + * * @throws InvalidAutoAssignActionTypeException * if the provided auto-assign {@link ActionType} is not valid * (neither FORCED, nor SOFT) - * - * @throws InvalidAutoAssignDistributionSetException - * if the provided auto-assign {@link DistributionSet} is not - * valid (incomplete or soft deleted) + * + * @throws IncompleteDistributionSetException + * if the provided auto-assign {@link DistributionSet} is + * incomplete + * + * @throws InvalidDistributionSetException + * if the provided auto-assign {@link DistributionSet} is + * invalidated */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) TargetFilterQuery updateAutoAssignDS( @NotNull @Valid AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate); + + /** + * Removes the given {@link DistributionSet} from all auto assignments. + * + * @param setId + * the {@link DistributionSet} to be removed from auto + * assignments. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + void cancelAutoAssignmentForDistributionSet(long setId); } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/RolloutStoppedEvent.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/RolloutStoppedEvent.java new file mode 100644 index 000000000..8c4a10cf7 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/RolloutStoppedEvent.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.event.remote; + +import java.util.Collection; + +import org.eclipse.hawkbit.repository.model.DistributionSet; + +/** + * + * Event that is published when a rollout is stopped due to invalidation of a + * {@link DistributionSet}. + */ +public class RolloutStoppedEvent extends RemoteTenantAwareEvent { + + private static final long serialVersionUID = 1L; + + private Collection rolloutGroupIds; + private long rolloutId; + + /** + * Default constructor. + */ + public RolloutStoppedEvent() { + // for serialization libs like jackson + } + + /** + * Constructor for json serialization. + * + * @param tenant + * the tenant + * @param entityId + * the entity id + * @param entityClass + * the entity class + * @param applicationId + * the origin application id + */ + public RolloutStoppedEvent(final String tenant, final String applicationId, final long rolloutId, + final Collection rolloutGroupIds) { + super(rolloutId, tenant, applicationId); + this.rolloutId = rolloutId; + this.rolloutGroupIds = rolloutGroupIds; + } + + public Collection getRolloutGroupIds() { + return rolloutGroupIds; + } + + public void setRolloutGroupIds(final Collection rolloutGroupIds) { + this.rolloutGroupIds = rolloutGroupIds; + } + + public long getRolloutId() { + return rolloutId; + } + + public void setRolloutId(final long rolloutId) { + this.rolloutId = rolloutId; + } + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/InvalidAutoAssignDistributionSetException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/InvalidAutoAssignDistributionSetException.java deleted file mode 100644 index 2c798d286..000000000 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/InvalidAutoAssignDistributionSetException.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2019 Bosch Software Innovations GmbH and others. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ - -package org.eclipse.hawkbit.repository.exception; - -import org.eclipse.hawkbit.exception.AbstractServerRtException; -import org.eclipse.hawkbit.exception.SpServerError; - -/** - * Thrown if a distribution set for auto-assignment is incomplete (i.e. - * mandatory modules are missing) or soft deleted. - */ -public class InvalidAutoAssignDistributionSetException extends AbstractServerRtException { - - private static final long serialVersionUID = 1L; - private static final SpServerError THIS_ERROR = SpServerError.SP_AUTO_ASSIGN_DISTRIBUTION_SET_INVALID; - - /** - * Default constructor. - */ - public InvalidAutoAssignDistributionSetException() { - super(THIS_ERROR); - } -} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/InvalidDistributionSetException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/InvalidDistributionSetException.java new file mode 100644 index 000000000..7b065e1ba --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/InvalidDistributionSetException.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.AbstractServerRtException; +import org.eclipse.hawkbit.exception.SpServerError; + +public class InvalidDistributionSetException extends AbstractServerRtException { + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new InvalidDistributionSetException with + * {@link SpServerError#SP_DS_INVALID} error. + */ + public InvalidDistributionSetException() { + super(SpServerError.SP_DS_INVALID); + } + + /** + * @param cause + * for the exception + */ + public InvalidDistributionSetException(final Throwable cause) { + super(SpServerError.SP_DS_INVALID, cause); + } + + /** + * @param message + * of the error + */ + public InvalidDistributionSetException(final String message) { + super(message, SpServerError.SP_DS_INVALID); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/StopRolloutException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/StopRolloutException.java new file mode 100644 index 000000000..053fdaeaf --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/StopRolloutException.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.AbstractServerRtException; +import org.eclipse.hawkbit.exception.SpServerError; + +/** + * {@link StopRolloutException} is thrown when an error occurs while stopping + * the rollout (due to an invalidation of distribution set). This could be + * caused by a long ongoing creation of a rollout. + */ +public class StopRolloutException extends AbstractServerRtException { + private static final long serialVersionUID = 1L; + + /** + * Creates a new StopRolloutException with + * {@link SpServerError#SP_STOP_ROLLOUT_FAILED} error. + */ + public StopRolloutException() { + super(SpServerError.SP_STOP_ROLLOUT_FAILED); + } + + /** + * Creates a new StopRolloutException with + * {@link SpServerError#SP_STOP_ROLLOUT_FAILED} error. + * + * @param cause + * for the exception + */ + public StopRolloutException(final Throwable cause) { + super(SpServerError.SP_STOP_ROLLOUT_FAILED, cause); + } + + /** + * Creates a new StopRolloutException with + * {@link SpServerError#SP_STOP_ROLLOUT_FAILED} error. + * + * @param message + * of the error + */ + public StopRolloutException(final String message) { + super(message, SpServerError.SP_STOP_ROLLOUT_FAILED); + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSet.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSet.java index 40f9ed3d2..49e38fac2 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSet.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSet.java @@ -16,7 +16,7 @@ import java.util.Set; * A {@link DistributionSet} defines a meta package that combines a set of * {@link SoftwareModule}s which have to be or are provisioned to a * {@link Target}. - * + * *

* A {@link Target} has exactly one target {@link DistributionSet} assigned. *

@@ -71,4 +71,10 @@ public interface DistributionSet extends NamedVersionedEntity { */ boolean isComplete(); + /** + * @return false if this {@link DistributionSet} is + * invalidated. + */ + boolean isValid(); + } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetFilter.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetFilter.java index 27565c305..3e80ada54 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetFilter.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetFilter.java @@ -20,6 +20,7 @@ public final class DistributionSetFilter { public static class DistributionSetFilterBuilder { private Boolean isDeleted; private Boolean isComplete; + private Boolean isValid; private DistributionSetType type; private String searchText; private String filterString; @@ -57,6 +58,11 @@ public final class DistributionSetFilter { return this; } + public DistributionSetFilterBuilder setIsValid(final Boolean isValid) { + this.isValid = isValid; + return this; + } + public DistributionSetFilterBuilder setSearchText(final String searchText) { this.searchText = searchText; return this; @@ -86,6 +92,7 @@ public final class DistributionSetFilter { private final Boolean isDeleted; private final Boolean isComplete; + private final Boolean isValid; private final DistributionSetType type; private final String searchText; private final String filterString; @@ -104,6 +111,7 @@ public final class DistributionSetFilter { public DistributionSetFilter(final DistributionSetFilterBuilder builder) { this.isDeleted = builder.isDeleted; this.isComplete = builder.isComplete; + this.isValid = builder.isValid; this.type = builder.type; this.searchText = builder.searchText; this.filterString = builder.filterString; @@ -129,6 +137,10 @@ public final class DistributionSetFilter { return isDeleted; } + public Boolean getIsValid() { + return isValid; + } + public String getSearchText() { return searchText; } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetInvalidation.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetInvalidation.java new file mode 100644 index 000000000..398766126 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetInvalidation.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.util.Collection; + +/** + * Holds the information about the invalidation of a distribution set + */ +public class DistributionSetInvalidation { + + private Collection distributionSetIds; + private CancelationType cancelationType; + private boolean cancelRollouts; + + /** + * Parametric constructor + * + * @param distributionSetIds + * defines which distribution sets should be canceled + * @param cancelationType + * defines if actions should be canceled + * @param cancelRollouts + * defines if rollouts should be canceled + */ + public DistributionSetInvalidation(final Collection distributionSetIds, final CancelationType cancelationType, + final boolean cancelRollouts) { + this.distributionSetIds = distributionSetIds; + this.cancelationType = cancelationType; + this.cancelRollouts = cancelRollouts; + } + + public Collection getDistributionSetIds() { + return distributionSetIds; + } + + public void setDistributionSetIds(final Collection distributionSetIds) { + this.distributionSetIds = distributionSetIds; + } + + public CancelationType getCancelationType() { + return cancelationType; + } + + public void setCancelationType(final CancelationType cancelationType) { + this.cancelationType = cancelationType; + } + + public boolean isCancelRollouts() { + return cancelRollouts; + } + + public void setCancelRollouts(final boolean cancelRollouts) { + this.cancelRollouts = cancelRollouts; + } + + /** + * Defines if and how actions should be canceled when invalidating a + * distribution set + */ + public enum CancelationType { + FORCE, SOFT, NONE; + } + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetInvalidationCount.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetInvalidationCount.java new file mode 100644 index 000000000..d0bdf37ac --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetInvalidationCount.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +/** + * object that holds information about the count of affected rollouts, + * auto-assignments and actions, when a list of distribution sets gets + * invalidated + */ +public class DistributionSetInvalidationCount { + + private final long rolloutsCount; + private final long autoAssignmentCount; + private final long actionCount; + + public DistributionSetInvalidationCount(final long rolloutsCount, final long autoAssignmentCount, + final long actionCount) { + this.rolloutsCount = rolloutsCount; + this.autoAssignmentCount = autoAssignmentCount; + this.actionCount = actionCount; + } + + public long getRolloutsCount() { + return rolloutsCount; + } + + public long getAutoAssignmentCount() { + return autoAssignmentCount; + } + + public long getActionCount() { + return actionCount; + } + +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java index 7e1c50e2c..dfd89cc78 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java @@ -17,7 +17,7 @@ import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus.Status; /** * Software update operations in large scale IoT scenarios with hundred of * thousands of devices require special handling. - * + * * That includes secure handling of large volumes of devices at rollout creation * time. Monitoring of the rollout progress. Emergency rollout shutdown in case * of problems on to many devices and reporting capabilities for a complete @@ -149,6 +149,11 @@ public interface Rollout extends NamedEntity { */ STOPPED, + /** + * Rollout is going to be stopped. + */ + STOPPING, + /** * Rollout is running. */ @@ -174,7 +179,7 @@ public interface Rollout extends NamedEntity { /** * Rollout could not be created due to errors, might be a database * problem during asynchronous creating. - * + * * @deprecated legacy status is not used anymore */ @Deprecated @@ -183,7 +188,7 @@ public interface Rollout extends NamedEntity { /** * Rollout could not be started due to errors, might be database problem * during asynchronous starting. - * + * * @deprecated legacy status is not used anymore */ @Deprecated diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/event/EventType.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/event/EventType.java index 6e9e7c2f3..fe4b626b7 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/event/EventType.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/event/EventType.java @@ -21,6 +21,7 @@ import org.eclipse.hawkbit.repository.event.remote.MultiActionAssignEvent; import org.eclipse.hawkbit.repository.event.remote.MultiActionCancelEvent; import org.eclipse.hawkbit.repository.event.remote.RolloutDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.RolloutGroupDeletedEvent; +import org.eclipse.hawkbit.repository.event.remote.RolloutStoppedEvent; import org.eclipse.hawkbit.repository.event.remote.SoftwareModuleDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.SoftwareModuleTypeDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; @@ -60,7 +61,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TenantConfigurationUpd * The {@link EventType} class declares the event-type and it's corresponding * encoding value in the payload of an remote header. The event-type is encoded * into the payload of the message which is distributed. - * + * * To encode and decode the event class type we need some conversation mapping * between the actual class and the corresponding integer value which is the * encoded value in the byte-payload. @@ -146,6 +147,9 @@ public class EventType { TYPES.put(40, TenantConfigurationCreatedEvent.class); TYPES.put(41, TenantConfigurationUpdatedEvent.class); TYPES.put(42, TenantConfigurationDeletedEvent.class); + + // rollout stopped due to invalidated distribution set + TYPES.put(43, RolloutStoppedEvent.class); } private int value; @@ -159,7 +163,7 @@ public class EventType { /** * Constructor. - * + * * @param value * the value to initialize */ @@ -177,7 +181,7 @@ public class EventType { /** * Returns a {@link EventType} based on the given class type. - * + * * @param clazz * the clazz type to retrieve the corresponding {@link EventType} * . diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutStatusCache.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutStatusCache.java index c5196d141..765678ee3 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutStatusCache.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutStatusCache.java @@ -18,6 +18,7 @@ import org.eclipse.hawkbit.cache.TenancyCacheManager; import org.eclipse.hawkbit.cache.TenantAwareCacheManager; import org.eclipse.hawkbit.repository.event.remote.RolloutDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.RolloutGroupDeletedEvent; +import org.eclipse.hawkbit.repository.event.remote.RolloutStoppedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.AbstractActionEvent; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup; @@ -67,7 +68,7 @@ public class RolloutStatusCache { /** * Retrieves cached list of {@link TotalTargetCountActionStatus} of * {@link Rollout}s. - * + * * @param rollouts * rolloutIds to retrieve cache entries for * @return map of cached entries @@ -81,7 +82,7 @@ public class RolloutStatusCache { /** * Retrieves cached list of {@link TotalTargetCountActionStatus} of * {@link Rollout}s. - * + * * @param rolloutId * to retrieve cache entries for * @return map of cached entries @@ -95,7 +96,7 @@ public class RolloutStatusCache { /** * Retrieves cached list of {@link TotalTargetCountActionStatus} of * {@link RolloutGroup}s. - * + * * @param rolloutGroups * rolloutGroupsIds to retrieve cache entries for * @return map of cached entries @@ -109,7 +110,7 @@ public class RolloutStatusCache { /** * Retrieves cached list of {@link TotalTargetCountActionStatus} of * {@link RolloutGroup}. - * + * * @param groupId * to retrieve cache entries for * @return map of cached entries @@ -123,7 +124,7 @@ public class RolloutStatusCache { /** * Put map of {@link TotalTargetCountActionStatus} for multiple * {@link Rollout}s into cache. - * + * * @param put * map of cached entries */ @@ -135,12 +136,12 @@ public class RolloutStatusCache { /** * Put {@link TotalTargetCountActionStatus} for one {@link Rollout}s into * cache. - * + * * @param rolloutId * the cache entries belong to * @param status * list to cache - * + * */ public void putRolloutStatus(final Long rolloutId, final List status) { final Cache cache = cacheManager.getCache(CACHE_RO_NAME); @@ -150,7 +151,7 @@ public class RolloutStatusCache { /** * Put {@link TotalTargetCountActionStatus} for multiple * {@link RolloutGroup}s into cache. - * + * * @param put * map of cached entries */ @@ -162,7 +163,7 @@ public class RolloutStatusCache { /** * Put {@link TotalTargetCountActionStatus} for multiple * {@link RolloutGroup}s into cache. - * + * * @param groupId * the cache entries belong to * @param status @@ -223,6 +224,18 @@ public class RolloutStatusCache { cache.evict(event.getEntityId()); } + @EventListener(classes = RolloutStoppedEvent.class) + void invalidateCachedTotalTargetCountOnRolloutStopped(final RolloutStoppedEvent event) { + final Cache cache = tenantAware.runAsTenant(event.getTenant(), () -> cacheManager.getCache(CACHE_RO_NAME)); + cache.evict(event.getRolloutId()); + + for (final long groupId : event.getRolloutGroupIds()) { + final Cache groupCache = tenantAware.runAsTenant(event.getTenant(), + () -> cacheManager.getCache(CACHE_GR_NAME)); + groupCache.evict(groupId); + } + } + /** * Evicts all caches for a given tenant. All caches under a certain tenant * gets evicted. diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java index 737151ab7..41589d6d0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java @@ -64,6 +64,28 @@ public interface ActionRepository extends BaseEntityRepository, */ Page findByDistributionSetId(Pageable pageable, Long dsId); + /** + * Retrieves all active {@link Action}s which are referring the given + * {@link DistributionSet}. + * + * @param set + * the {@link DistributionSet} on which will be filtered + * @return the found {@link Action}s + */ + List findByDistributionSetAndActiveIsTrue(DistributionSet set); + + /** + * Retrieves all active {@link Action}s which are referring the given + * {@link DistributionSet} and are not in the given state + * + * @param set + * the {@link DistributionSet} on which will be filtered + * @param status + * the state the actions should not have + * @return the found {@link Action}s + */ + List findByDistributionSetAndActiveIsTrueAndStatusIsNot(DistributionSet set, Status status); + /** * Retrieves all {@link Action}s which are referring the given * {@link Target}. @@ -103,7 +125,7 @@ public interface ActionRepository extends BaseEntityRepository, * Retrieves the active {@link Action}s with the highest weights that refer * to the given {@link Target}. If {@link Action}s have the same weight they * are ordered ascending by ID (oldest ones first). - * + * * @param pageable * pageable * @param controllerId @@ -117,7 +139,7 @@ public interface ActionRepository extends BaseEntityRepository, /** * Retrieves the active {@link Action}s with the lowest IDs (the oldest one) * whose weight is null and that that refers to the given {@link Target}. - * + * * @param pageable * pageable * @param controllerId @@ -131,7 +153,7 @@ public interface ActionRepository extends BaseEntityRepository, /** * Checks if an active action exists for given * {@link Target#getControllerId()}. - * + * * @param controllerId * of target to check * @return true if an active action for the target exists. @@ -264,10 +286,9 @@ public interface ActionRepository extends BaseEntityRepository, List findByExternalRefInAndActive(@Param("externalRefs") List externalRefs, @Param("active") boolean active); - /** * Retrieves an {@link Action} that matches the queried externalRef. - * + * * @param externalRef * of the action. See {@link Action#getExternalRef()} * @return the found {@link Action} @@ -351,6 +372,27 @@ public interface ActionRepository extends BaseEntityRepository, */ Long countByDistributionSetId(Long distributionSet); + /** + * Counts all active {@link Action}s referring to the given DistributionSet. + * + * @param distributionSet + * DistributionSet to count the {@link Action}s from + * @return the count of actions referring to the given distributionSet + */ + Long countByDistributionSetIdAndActiveIsTrue(Long distributionSet); + + /** + * Counts all active {@link Action}s referring to the given DistributionSet + * that are not in a given state. + * + * @param distributionSet + * DistributionSet to count the {@link Action}s from + * @param status + * the state the actions should not have + * @return the count of actions referring to the given distributionSet + */ + Long countByDistributionSetIdAndActiveIsTrueAndStatusIsNot(Long distributionSet, Status status); + /** * Counts all actions referring to a given rollout and rolloutgroup which * are currently not in the given status. An in-clause statement does not @@ -396,7 +438,7 @@ public interface ActionRepository extends BaseEntityRepository, /** * Counts all actions referring to a given rollout and status. - * + * * @param rolloutId * the ID of the rollout the actions belong to * @param status @@ -409,7 +451,7 @@ public interface ActionRepository extends BaseEntityRepository, /** * Returns {@code true} if actions for the given rollout exists, otherwise * {@code false} - * + * * @param rolloutId * the ID of the rollout the actions belong to * @return {@code true} if actions for the given rollout exists, otherwise @@ -421,7 +463,7 @@ public interface ActionRepository extends BaseEntityRepository, /** * Returns {@code true} if actions for the given rollout exists, otherwise * {@code false} - * + * * @param rolloutId * the ID of the rollout the actions belong to * @param status @@ -471,7 +513,7 @@ public interface ActionRepository extends BaseEntityRepository, /** * Retrieves all actions for a specific rollout and in a specific status. - * + * * @param pageable * page parameters * @param rolloutId @@ -528,7 +570,7 @@ public interface ActionRepository extends BaseEntityRepository, /** * Deletes all actions with the given IDs. - * + * * @param actionIDs * the IDs of the actions to be deleted. */ diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java index 37fd8222a..90489f6d6 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java @@ -35,6 +35,7 @@ import javax.persistence.criteria.Root; import org.eclipse.hawkbit.repository.ActionFields; import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.RepositoryProperties; @@ -43,7 +44,6 @@ import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEv import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.ForceQuitActionNotAllowedException; -import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; @@ -67,6 +67,7 @@ import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.Target; @@ -130,6 +131,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl } private final EntityManager entityManager; + private final DistributionSetManagement distributionSetManagement; private final DistributionSetRepository distributionSetRepository; private final TargetRepository targetRepository; private final ActionStatusRepository actionStatusRepository; @@ -146,6 +148,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl private final RetryTemplate retryTemplate; protected JpaDeploymentManagement(final EntityManager entityManager, final ActionRepository actionRepository, + final DistributionSetManagement distributionSetManagement, final DistributionSetRepository distributionSetRepository, final TargetRepository targetRepository, final ActionStatusRepository actionStatusRepository, final AuditorAware auditorProvider, final EventPublisherHolder eventPublisherHolder, final AfterTransactionCommitExecutor afterCommit, @@ -156,6 +159,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl super(actionRepository, repositoryProperties); this.entityManager = entityManager; this.distributionSetRepository = distributionSetRepository; + this.distributionSetManagement = distributionSetManagement; this.targetRepository = targetRepository; this.actionStatusRepository = actionStatusRepository; this.auditorProvider = auditorProvider; @@ -254,8 +258,8 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl /** * method assigns the {@link DistributionSet} to all {@link Target}s by * their IDs with a specific {@link ActionType} and {@code forcetime}. - * - * + * + * * In case the update was executed offline (i.e. not managed by hawkBit) the * handling differs my means that:
* A. it ignores targets completely that are in @@ -285,7 +289,8 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl final Collection targetsWithActionType, final String actionMessage, final AbstractDsAssignmentStrategy assignmentStrategy) { - final JpaDistributionSet distributionSetEntity = getAndValidateDsById(dsID); + final JpaDistributionSet distributionSetEntity = (JpaDistributionSet) distributionSetManagement + .getValidAndComplete(dsID); final List providedTargetIds = targetsWithActionType.stream().map(TargetWithActionType::getControllerId) .distinct().collect(Collectors.toList()); @@ -363,18 +368,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl return new DistributionSetAssignmentResult(distributionSet, alreadyAssignedTargetsCount, assignedActions); } - private JpaDistributionSet getAndValidateDsById(final Long dsID) { - final JpaDistributionSet distributionSet = distributionSetRepository.findById(dsID) - .orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, dsID)); - - if (!distributionSet.isComplete()) { - throw new IncompleteDistributionSetException("Distribution set of type " - + distributionSet.getType().getKey() + " is incomplete: " + distributionSet.getId()); - } - - return distributionSet; - } - private void checkQuotaForAssignment(final Collection deploymentRequests) { if (!deploymentRequests.isEmpty()) { enforceMaxAssignmentsPerRequest(deploymentRequests.size()); @@ -656,8 +649,8 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl } private Specification createSpecificationFor(final String controllerId, final String rsqlParam) { - final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, ActionFields.class, virtualPropertyReplacer, - database); + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, ActionFields.class, + virtualPropertyReplacer, database); return (root, query, cb) -> cb.and(spec.toPredicate(root, query, cb), cb.equal(root.get(JpaAction_.target).get(JpaTarget_.controllerId), controllerId)); } @@ -779,6 +772,17 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl return actionRepository.count(); } + @Override + public long countActionsByDistributionSetIdAndActiveIsTrue(final Long distributionSet) { + return actionRepository.countByDistributionSetIdAndActiveIsTrue(distributionSet); + } + + @Override + public long countActionsByDistributionSetIdAndActiveIsTrueAndStatusIsNot(final Long distributionSet, + final Status status) { + return actionRepository.countByDistributionSetIdAndActiveIsTrueAndStatusIsNot(distributionSet, status); + } + @Override public Slice findActionsByDistributionSet(final Pageable pageable, final long dsId) { throwExceptionIfDistributionSetDoesNotExist(dsId); @@ -832,7 +836,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl } @Override - public boolean hasPendingCancellations(String controllerId) { + public boolean hasPendingCancellations(final String controllerId) { return actionRepository.existsByTargetControllerIdAndStatusAndActiveIsTrue(controllerId, Action.Status.CANCELING); } @@ -881,4 +885,21 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl return template; } + + @Override + @Transactional + public void cancelActionsForDistributionSet(final CancelationType cancelationType, final DistributionSet set) { + actionRepository.findByDistributionSetAndActiveIsTrueAndStatusIsNot(set, Status.CANCELING).forEach(action -> { + final JpaAction jpaAction = (JpaAction) action; + cancelAction(jpaAction.getId()); + LOG.debug("Action {} canceled", jpaAction.getId()); + }); + if (cancelationType == CancelationType.FORCE) { + actionRepository.findByDistributionSetAndActiveIsTrue(set).forEach(action -> { + final JpaAction jpaAction = (JpaAction) action; + forceQuitAction(jpaAction.getId()); + LOG.debug("Action {} force canceled", jpaAction.getId()); + }); + } + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetInvalidationManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetInvalidationManagement.java new file mode 100644 index 000000000..cbe201826 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetInvalidationManagement.java @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa; + +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement; +import org.eclipse.hawkbit.repository.DistributionSetManagement; +import org.eclipse.hawkbit.repository.RepositoryProperties; +import org.eclipse.hawkbit.repository.RolloutManagement; +import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; +import org.eclipse.hawkbit.repository.exception.StopRolloutException; +import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidationCount; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.integration.support.locks.LockRegistry; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * Jpa implementation for {@link DistributionSetInvalidationManagement} + * + */ +public class JpaDistributionSetInvalidationManagement implements DistributionSetInvalidationManagement { + + private static final Logger LOG = LoggerFactory.getLogger(JpaDistributionSetInvalidationManagement.class); + + private final DistributionSetManagement distributionSetManagement; + private final RolloutManagement rolloutManagement; + private final DeploymentManagement deploymentManagement; + private final TargetFilterQueryManagement targetFilterQueryManagement; + private final PlatformTransactionManager txManager; + private final RepositoryProperties repositoryProperties; + private final TenantAware tenantAware; + private final LockRegistry lockRegistry; + + protected JpaDistributionSetInvalidationManagement(final DistributionSetManagement distributionSetManagement, + final RolloutManagement rolloutManagement, final DeploymentManagement deploymentManagement, + final TargetFilterQueryManagement targetFilterQueryManagement, final PlatformTransactionManager txManager, + final RepositoryProperties repositoryProperties, final TenantAware tenantAware, + final LockRegistry lockRegistry) { + this.distributionSetManagement = distributionSetManagement; + this.rolloutManagement = rolloutManagement; + this.deploymentManagement = deploymentManagement; + this.targetFilterQueryManagement = targetFilterQueryManagement; + this.txManager = txManager; + this.repositoryProperties = repositoryProperties; + this.tenantAware = tenantAware; + this.lockRegistry = lockRegistry; + } + + @Override + public void invalidateDistributionSet(final DistributionSetInvalidation distributionSetInvalidation) { + LOG.debug("Invalidate distribution sets {}", distributionSetInvalidation.getDistributionSetIds()); + final String tenant = tenantAware.getCurrentTenant(); + + if (shouldRolloutsBeCanceled(distributionSetInvalidation.getCancelationType(), + distributionSetInvalidation.isCancelRollouts())) { + final String handlerId = JpaRolloutManagement.createRolloutLockKey(tenant); + final Lock lock = lockRegistry.obtain(handlerId); + try { + if (!lock.tryLock(repositoryProperties.getDsInvalidationLockTimeout(), TimeUnit.SECONDS)) { + throw new StopRolloutException("Timeout while trying to invalidate distribution sets"); + } + try { + invalidateDistributionSetsInTransaction(distributionSetInvalidation, tenant); + } finally { + lock.unlock(); + } + } catch (final InterruptedException e) { + LOG.error("InterruptedException while invalidating distribution sets {}!", + distributionSetInvalidation.getDistributionSetIds(), e); + Thread.currentThread().interrupt(); + } + } else { + // no lock is needed as no rollout will be stopped + invalidateDistributionSetsInTransaction(distributionSetInvalidation, tenant); + } + } + + private void invalidateDistributionSetsInTransaction(final DistributionSetInvalidation distributionSetInvalidation, + final String tenant) { + DeploymentHelper.runInNewTransaction(txManager, tenant + "-invalidateDS", status -> { + distributionSetInvalidation.getDistributionSetIds().forEach(setId -> invalidateDistributionSet(setId, + distributionSetInvalidation.getCancelationType(), distributionSetInvalidation.isCancelRollouts())); + return 0; + }); + } + + private void invalidateDistributionSet(final long setId, final CancelationType cancelationType, + final boolean cancelRollouts) { + final DistributionSet set = distributionSetManagement.getValidAndComplete(setId); + distributionSetManagement.invalidate(set); + LOG.debug("Distribution set {} set to invalid", setId); + + if (shouldRolloutsBeCanceled(cancelationType, cancelRollouts)) { + rolloutManagement.cancelRolloutsForDistributionSet(set); + } + + if (cancelationType != CancelationType.NONE) { + deploymentManagement.cancelActionsForDistributionSet(cancelationType, set); + } + + targetFilterQueryManagement.cancelAutoAssignmentForDistributionSet(setId); + } + + private static boolean shouldRolloutsBeCanceled(final CancelationType cancelationType, + final boolean cancelRollouts) { + return cancelationType != CancelationType.NONE || cancelRollouts; + } + + @Override + public DistributionSetInvalidationCount countEntitiesForInvalidation( + final DistributionSetInvalidation distributionSetInvalidation) { + final Collection setIds = distributionSetInvalidation.getDistributionSetIds(); + final long rolloutsCount = shouldRolloutsBeCanceled(distributionSetInvalidation.getCancelationType(), + distributionSetInvalidation.isCancelRollouts()) ? countRolloutsForInvalidation(setIds) : 0; + final long autoAssignmentsCount = countAutoAssignmentsForInvalidation(setIds); + final long actionsCount = countActionsForInvalidation(setIds, distributionSetInvalidation.getCancelationType()); + + return new DistributionSetInvalidationCount(rolloutsCount, autoAssignmentsCount, actionsCount); + } + + private long countRolloutsForInvalidation(final Collection setIds) { + return setIds.stream().mapToLong(rolloutManagement::countByDistributionSetIdAndRolloutIsStoppable).sum(); + } + + private long countAutoAssignmentsForInvalidation(final Collection setIds) { + return setIds.stream().mapToLong(targetFilterQueryManagement::countByAutoAssignDistributionSetId).sum(); + } + + private long countActionsForInvalidation(final Collection setIds, final CancelationType cancelationType) { + long affectedActionsByDSInvalidation = 0; + if (cancelationType == CancelationType.FORCE) { + affectedActionsByDSInvalidation = countActionsForForcedInvalidation(setIds); + } else if (cancelationType == CancelationType.SOFT) { + affectedActionsByDSInvalidation = countActionsForSoftInvalidation(setIds); + } + return affectedActionsByDSInvalidation; + } + + private long countActionsForForcedInvalidation(final Collection setIds) { + return setIds.stream().mapToLong(deploymentManagement::countActionsByDistributionSetIdAndActiveIsTrue).sum(); + } + + private long countActionsForSoftInvalidation(final Collection setIds) { + return setIds.stream().mapToLong(distributionSet -> deploymentManagement + .countActionsByDistributionSetIdAndActiveIsTrueAndStatusIsNot(distributionSet, Status.CANCELING)).sum(); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java index b44e9d73e..061fb9251 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java @@ -32,6 +32,8 @@ import org.eclipse.hawkbit.repository.event.remote.DistributionSetDeletedEvent; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; import org.eclipse.hawkbit.repository.jpa.builder.JpaDistributionSetCreate; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; @@ -213,7 +215,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { public DistributionSet update(final DistributionSetUpdate u) { final GenericDistributionSetUpdate update = (GenericDistributionSetUpdate) u; - final JpaDistributionSet set = findDistributionSetAndThrowExceptionIfNotFound(update.getId()); + final JpaDistributionSet set = (JpaDistributionSet) getValid(update.getId()); update.getName().ifPresent(set::setName); update.getDescription().ifPresent(set::setDescription); @@ -228,11 +230,6 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { return distributionSetRepository.save(set); } - private JpaDistributionSet findDistributionSetAndThrowExceptionIfNotFound(final Long setId) { - return (JpaDistributionSet) get(setId) - .orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, setId)); - } - private JpaSoftwareModule findSoftwareModuleAndThrowExceptionIfNotFound(final Long moduleId) { return softwareModuleRepository.findById(moduleId) .orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, moduleId)); @@ -315,7 +312,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { assertDistributionSetIsNotAssignedToTargets(setId); - final JpaDistributionSet set = findDistributionSetAndThrowExceptionIfNotFound(setId); + final JpaDistributionSet set = (JpaDistributionSet) getValid(setId); assertSoftwareModuleQuota(setId, modules.size()); @@ -329,7 +326,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { @Retryable(include = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public DistributionSet unassignSoftwareModule(final long setId, final long moduleId) { - final JpaDistributionSet set = findDistributionSetAndThrowExceptionIfNotFound(setId); + final JpaDistributionSet set = (JpaDistributionSet) getValid(setId); final JpaSoftwareModule module = findSoftwareModuleAndThrowExceptionIfNotFound(moduleId); assertDistributionSetIsNotAssignedToTargets(setId); @@ -531,7 +528,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { * of the DS to touch */ private JpaDistributionSet touch(final Long distId) { - return touch(get(distId).orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, distId))); + return touch(getValid(distId)); } @Override @@ -595,7 +592,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { private static List> buildDistributionSetSpecifications( final DistributionSetFilter distributionSetFilter) { - final List> specList = Lists.newArrayListWithExpectedSize(8); + final List> specList = Lists.newArrayListWithExpectedSize(9); Specification spec; @@ -609,6 +606,11 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { specList.add(spec); } + if (distributionSetFilter.getIsValid() != null) { + spec = DistributionSetSpecification.isValid(distributionSetFilter.getIsValid()); + specList.add(spec); + } + if (distributionSetFilter.getType() != null) { spec = DistributionSetSpecification.byType(distributionSetFilter.getType()); specList.add(spec); @@ -781,8 +783,8 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { public Page findByRsqlAndTag(final Pageable pageable, final String rsqlParam, final long tagId) { throwEntityNotFoundExceptionIfDsTagDoesNotExist(tagId); - final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, DistributionSetFields.class, - virtualPropertyReplacer, database); + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, + DistributionSetFields.class, virtualPropertyReplacer, database); return convertDsPage(findByCriteriaAPI(pageable, Arrays.asList(spec, DistributionSetSpecification.hasTag(tagId), DistributionSetSpecification.isDeleted(false))), pageable); @@ -797,8 +799,8 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { @Override public Page findByRsql(final Pageable pageable, final String rsqlParam) { - final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, DistributionSetFields.class, - virtualPropertyReplacer, database); + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, + DistributionSetFields.class, virtualPropertyReplacer, database); return convertDsPage( findByCriteriaAPI(pageable, Arrays.asList(spec, DistributionSetSpecification.isDeleted(false))), @@ -815,4 +817,41 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { return distributionSetRepository.existsById(id); } + @Override + public DistributionSet getOrElseThrowException(final long id) { + return get(id).orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, id)); + } + + @Override + public DistributionSet getValidAndComplete(final long id) { + final DistributionSet distributionSet = getValid(id); + + if (!distributionSet.isComplete()) { + throw new IncompleteDistributionSetException("Distribution set of type " + + distributionSet.getType().getKey() + " is incomplete: " + distributionSet.getId()); + } + + return distributionSet; + } + + @Override + public DistributionSet getValid(final long id) { + final DistributionSet distributionSet = getOrElseThrowException(id); + + if (!distributionSet.isValid()) { + throw new InvalidDistributionSetException("Distribution set of type " + distributionSet.getType().getKey() + + " is invalid: " + distributionSet.getId()); + } + + return distributionSet; + } + + @Override + @Transactional + public void invalidate(final DistributionSet set) { + final JpaDistributionSet jpaSet = (JpaDistributionSet) set; + jpaSet.invalidate(); + distributionSetRepository.save(jpaSet); + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java index 1e1969adc..8ab55e4e6 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java @@ -25,6 +25,7 @@ import org.eclipse.hawkbit.repository.RolloutHelper; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.event.remote.RolloutGroupDeletedEvent; +import org.eclipse.hawkbit.repository.event.remote.RolloutStoppedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.RolloutUpdatedEvent; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; @@ -155,6 +156,9 @@ public class JpaRolloutExecutor implements RolloutExecutor { case RUNNING: handleRunningRollout((JpaRollout) rollout); break; + case STOPPING: + handleStopRollout((JpaRollout) rollout); + break; default: LOGGER.error("Rollout in status {} not supposed to be handled!", rollout.getStatus()); break; @@ -244,6 +248,39 @@ public class JpaRolloutExecutor implements RolloutExecutor { sendRolloutGroupDeletedEvents(rollout); } + private void handleStopRollout(final JpaRollout rollout) { + LOGGER.debug("handleStopRollout called for {}", rollout.getId()); + // clean up all scheduled actions + final Slice scheduledActions = findScheduledActionsByRollout(rollout); + deleteScheduledActions(rollout, scheduledActions); + + // avoid another scheduler round and re-check if all scheduled actions + // has been cleaned up. we flush first to ensure that the we include the + // deletion above + entityManager.flush(); + final boolean hasScheduledActionsLeft = actionRepository.countByRolloutIdAndStatus(rollout.getId(), + Status.SCHEDULED) > 0; + + if (hasScheduledActionsLeft) { + return; + } + + rolloutGroupRepository.findByRolloutAndStatusNotIn(rollout, + Arrays.asList(RolloutGroupStatus.FINISHED, RolloutGroupStatus.ERROR)).forEach(rolloutGroup -> { + rolloutGroup.setStatus(RolloutGroupStatus.FINISHED); + rolloutGroupRepository.save(rolloutGroup); + }); + + rollout.setStatus(RolloutStatus.FINISHED); + rolloutRepository.save(rollout); + + final List groupIds = rollout.getRolloutGroups().stream().map(RolloutGroup::getId) + .collect(Collectors.toList()); + + afterCommit.afterCommit(() -> eventPublisherHolder.getEventPublisher().publishEvent(new RolloutStoppedEvent( + tenantAware.getCurrentTenant(), eventPublisherHolder.getApplicationId(), rollout.getId(), groupIds))); + } + private void handleReadyRollout(final Rollout rollout) { if (rollout.getStartAt() != null && rollout.getStartAt() <= System.currentTimeMillis()) { LOGGER.debug( diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java index 1d45755d0..6d4aa2421 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java @@ -100,7 +100,12 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { private static final Logger LOGGER = LoggerFactory.getLogger(JpaRolloutManagement.class); private static final List ACTIVE_ROLLOUTS = Arrays.asList(RolloutStatus.CREATING, - RolloutStatus.DELETING, RolloutStatus.STARTING, RolloutStatus.READY, RolloutStatus.RUNNING); + RolloutStatus.DELETING, RolloutStatus.STARTING, RolloutStatus.READY, RolloutStatus.RUNNING, + RolloutStatus.STOPPING); + + private static final List ROLLOUT_STATUS_STOPPABLE = Arrays.asList(RolloutStatus.RUNNING, + RolloutStatus.CREATING, RolloutStatus.PAUSED, RolloutStatus.READY, RolloutStatus.STARTING, + RolloutStatus.WAITING_FOR_APPROVAL, RolloutStatus.APPROVAL_DENIED); @Autowired private RolloutRepository rolloutRepository; @@ -126,8 +131,8 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { private final Database database; - public JpaRolloutManagement(final TargetManagement targetManagement, final DeploymentManagement deploymentManagement, - final RolloutGroupManagement rolloutGroupManagement, + public JpaRolloutManagement(final TargetManagement targetManagement, + final DeploymentManagement deploymentManagement, final RolloutGroupManagement rolloutGroupManagement, final DistributionSetManagement distributionSetManagement, final ApplicationContext context, final EventPublisherHolder eventPublisherHolder, final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry, @@ -151,7 +156,8 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { @Override public Page findByRsql(final Pageable pageable, final String rsqlParam, final boolean deleted) { final List> specList = Lists.newArrayListWithExpectedSize(2); - specList.add(RSQLUtility.buildRsqlSpecification(rsqlParam, RolloutFields.class, virtualPropertyReplacer, database)); + specList.add( + RSQLUtility.buildRsqlSpecification(rsqlParam, RolloutFields.class, virtualPropertyReplacer, database)); specList.add(RolloutSpecification.isDeletedWithDistributionSet(deleted)); return JpaRolloutHelper.convertPage(findByCriteriaAPI(pageable, specList), pageable); @@ -400,7 +406,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { final String tenant = tenantAware.getCurrentTenant(); - final String handlerId = tenant + "-rollout"; + final String handlerId = createRolloutLockKey(tenant); final Lock lock = lockRegistry.obtain(handlerId); if (!lock.tryLock()) { return; @@ -414,6 +420,10 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { } } + public static String createRolloutLockKey(final String tenant) { + return tenant + "-rollout"; + } + private long handleRollout(final long rolloutId) { final JpaRollout rollout = rolloutRepository.findById(rolloutId) .orElseThrow(() -> new EntityNotFoundException(Rollout.class, rolloutId)); @@ -451,6 +461,11 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { return rolloutRepository.count(JpaRolloutHelper.likeNameOrDescription(searchText, false)); } + @Override + public long countByDistributionSetIdAndRolloutIsStoppable(final long setId) { + return rolloutRepository.countByDistributionSetIdAndStatusIn(setId, ROLLOUT_STATUS_STOPPABLE); + } + @Override public Slice findByFiltersWithDetailedStatus(final Pageable pageable, final String searchText, final boolean deleted) { @@ -483,8 +498,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { // null rollout.setStartAt(update.getStartAt().orElse(null)); update.getSet().ifPresent(setId -> { - final DistributionSet set = distributionSetManagement.get(setId) - .orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, setId)); + final DistributionSet set = distributionSetManagement.getValidAndComplete(setId); rollout.setDistributionSet(set); }); @@ -596,7 +610,20 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { } private void runInUserContext(final BaseEntity rollout, final Runnable handler) { - DeploymentHelper.runInNonSystemContext(handler, () -> Objects.requireNonNull(rollout.getCreatedBy()), tenantAware); + DeploymentHelper.runInNonSystemContext(handler, () -> Objects.requireNonNull(rollout.getCreatedBy()), + tenantAware); + } + + @Override + @Transactional + public void cancelRolloutsForDistributionSet(final DistributionSet set) { + // stop all rollouts for this distribution set + rolloutRepository.findByDistributionSetAndStatusIn(set, ROLLOUT_STATUS_STOPPABLE).forEach(rollout -> { + final JpaRollout jpaRollout = (JpaRollout) rollout; + jpaRollout.setStatus(RolloutStatus.STOPPING); + rolloutRepository.save(jpaRollout); + LOGGER.debug("Rollout {} stopped", jpaRollout.getId()); + }); } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java index 1c169109d..9620274a0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java @@ -26,7 +26,6 @@ import org.eclipse.hawkbit.repository.builder.TargetFilterQueryCreate; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryUpdate; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException; -import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignDistributionSetException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryCreate; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; @@ -72,7 +71,7 @@ import cz.jirutka.rsql.parser.RSQLParserException; public class JpaTargetFilterQueryManagement implements TargetFilterQueryManagement { private static final Logger LOGGER = LoggerFactory.getLogger(JpaTargetFilterQueryManagement.class); - + private final TargetFilterQueryRepository targetFilterQueryRepository; private final TargetManagement targetManagement; @@ -147,6 +146,11 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme return targetFilterQueryRepository.count(); } + @Override + public long countByAutoAssignDistributionSetId(final long autoAssignDistributionSetId) { + return targetFilterQueryRepository.countByAutoAssignDistributionSetId(autoAssignDistributionSetId); + } + private static Page convertPage(final Page findAll, final Pageable pageable) { return new PageImpl<>(new ArrayList<>(findAll.getContent()), pageable, findAll.getTotalElements()); @@ -165,8 +169,8 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme public Page findByRsql(final Pageable pageable, final String rsqlFilter) { List> specList = Collections.emptyList(); if (!StringUtils.isEmpty(rsqlFilter)) { - specList = Collections.singletonList( - RSQLUtility.buildRsqlSpecification(rsqlFilter, TargetFilterQueryFields.class, virtualPropertyReplacer, database)); + specList = Collections.singletonList(RSQLUtility.buildRsqlSpecification(rsqlFilter, + TargetFilterQueryFields.class, virtualPropertyReplacer, database)); } return convertPage(findTargetFilterQueryByCriteriaAPI(pageable, specList), pageable); } @@ -185,13 +189,13 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme final String rsqlFilter) { final List> specList = Lists.newArrayListWithExpectedSize(2); - final DistributionSet distributionSet = findDistributionSetAndThrowExceptionIfNotFound(setId); + final DistributionSet distributionSet = distributionSetManagement.getOrElseThrowException(setId); specList.add(TargetFilterQuerySpecification.byAutoAssignDS(distributionSet)); if (!StringUtils.isEmpty(rsqlFilter)) { - specList.add( - RSQLUtility.buildRsqlSpecification(rsqlFilter, TargetFilterQueryFields.class, virtualPropertyReplacer, database)); + specList.add(RSQLUtility.buildRsqlSpecification(rsqlFilter, TargetFilterQueryFields.class, + virtualPropertyReplacer, database)); } return convertPage(findTargetFilterQueryByCriteriaAPI(pageable, specList), pageable); } @@ -264,8 +268,9 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme // specify an // auto-assign distribution set when creating a target filter query assertMaxTargetsQuota(targetFilterQuery.getQuery()); - final JpaDistributionSet ds = findDistributionSetAndThrowExceptionIfNotFound(update.getDsId()); - verifyDistributionSetAndThrowExceptionIfNotValid(ds); + final JpaDistributionSet ds = (JpaDistributionSet) distributionSetManagement + .getValidAndComplete(update.getDsId()); + verifyDistributionSetAndThrowExceptionIfDeleted(ds); targetFilterQuery.setAutoAssignDistributionSet(ds); targetFilterQuery.setAutoAssignInitiatedBy(tenantAware.getCurrentUsername()); targetFilterQuery.setAutoAssignActionType(sanitizeAutoAssignActionType(update.getActionType())); @@ -274,9 +279,9 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme return targetFilterQueryRepository.save(targetFilterQuery); } - private static void verifyDistributionSetAndThrowExceptionIfNotValid(final DistributionSet distributionSet) { - if (!distributionSet.isComplete() || distributionSet.isDeleted()) { - throw new InvalidAutoAssignDistributionSetException(); + private static void verifyDistributionSetAndThrowExceptionIfDeleted(final DistributionSet distributionSet) { + if (distributionSet.isDeleted()) { + throw new EntityNotFoundException(DistributionSet.class, distributionSet.getId()); } } @@ -292,11 +297,6 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme return actionType; } - private JpaDistributionSet findDistributionSetAndThrowExceptionIfNotFound(final Long setId) { - return (JpaDistributionSet) distributionSetManagement.get(setId) - .orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, setId)); - } - private JpaTargetFilterQuery findTargetFilterQueryOrThrowExceptionIfNotFound(final Long queryId) { return targetFilterQueryRepository.findById(queryId) .orElseThrow(() -> new EntityNotFoundException(TargetFilterQuery.class, queryId)); @@ -317,4 +317,11 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme QuotaHelper.assertAssignmentQuota(targetManagement.countByRsql(query), quotaManagement.getMaxTargetsPerAutoAssignment(), Target.class, TargetFilterQuery.class); } + + @Override + @Transactional + public void cancelAutoAssignmentForDistributionSet(final long setId) { + targetFilterQueryRepository.unsetAutoAssignDistributionSetAndActionType(setId); + LOGGER.debug("Auto assignments for distribution sets {} deactivated", setId); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index b9d8e66e4..9fbdfee4b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -82,8 +82,8 @@ import org.eclipse.hawkbit.repository.jpa.rollout.condition.PauseRolloutGroupAct import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRolloutGroupSuccessAction; import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupErrorCondition; import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupSuccessCondition; -import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParserValidationOracle; import org.eclipse.hawkbit.repository.jpa.rsql.DefaultRsqlVisitorFactory; +import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParserValidationOracle; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.Rollout; @@ -238,13 +238,14 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { } @Bean - TargetBuilder targetBuilder(final TargetTypeManagement targetTypeManagement){ + TargetBuilder targetBuilder(final TargetTypeManagement targetTypeManagement) { return new JpaTargetBuilder(targetTypeManagement); } /** * @param dsTypeManagement - * for loading {@link TargetType#getCompatibleDistributionSetTypes()} + * for loading + * {@link TargetType#getCompatibleDistributionSetTypes()} * @return TargetTypeBuilder bean */ @Bean @@ -434,7 +435,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { @Override @Bean public PlatformTransactionManager transactionManager( - ObjectProvider transactionManagerCustomizers) { + final ObjectProvider transactionManagerCustomizers) { return new MultiTenantJpaTransactionManager(); } @@ -485,12 +486,12 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { DistributionSetTypeManagement distributionSetTypeManagement( final DistributionSetTypeRepository distributionSetTypeRepository, final SoftwareModuleTypeRepository softwareModuleTypeRepository, - final DistributionSetRepository distributionSetRepository, - final TargetTypeRepository targetTypeRepository, + final DistributionSetRepository distributionSetRepository, final TargetTypeRepository targetTypeRepository, final VirtualPropertyReplacer virtualPropertyReplacer, final JpaProperties properties, final QuotaManagement quotaManagement) { return new JpaDistributionSetTypeManagement(distributionSetTypeRepository, softwareModuleTypeRepository, - distributionSetRepository, targetTypeRepository, virtualPropertyReplacer, properties.getDatabase(), quotaManagement); + distributionSetRepository, targetTypeRepository, virtualPropertyReplacer, properties.getDatabase(), + quotaManagement); } /** @@ -541,13 +542,15 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final TargetRepository targetRepository, final TargetMetadataRepository targetMetadataRepository, final RolloutGroupRepository rolloutGroupRepository, final DistributionSetRepository distributionSetRepository, - final TargetFilterQueryRepository targetFilterQueryRepository, final TargetTypeRepository targetTypeRepository, - final TargetTagRepository targetTagRepository, final EventPublisherHolder eventPublisherHolder, - final TenantAware tenantAware, final AfterTransactionCommitExecutor afterCommit, - final VirtualPropertyReplacer virtualPropertyReplacer, final JpaProperties properties) { - return new JpaTargetManagement(entityManager, quotaManagement, targetRepository, targetTypeRepository, targetMetadataRepository, - rolloutGroupRepository, distributionSetRepository, targetFilterQueryRepository, targetTagRepository, - eventPublisherHolder, tenantAware, afterCommit, virtualPropertyReplacer, properties.getDatabase()); + final TargetFilterQueryRepository targetFilterQueryRepository, + final TargetTypeRepository targetTypeRepository, final TargetTagRepository targetTagRepository, + final EventPublisherHolder eventPublisherHolder, final TenantAware tenantAware, + final AfterTransactionCommitExecutor afterCommit, final VirtualPropertyReplacer virtualPropertyReplacer, + final JpaProperties properties) { + return new JpaTargetManagement(entityManager, quotaManagement, targetRepository, targetTypeRepository, + targetMetadataRepository, rolloutGroupRepository, distributionSetRepository, + targetFilterQueryRepository, targetTagRepository, eventPublisherHolder, tenantAware, afterCommit, + virtualPropertyReplacer, properties.getDatabase()); } /** @@ -718,17 +721,17 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { @ConditionalOnMissingBean DeploymentManagement deploymentManagement(final EntityManager entityManager, final ActionRepository actionRepository, final DistributionSetRepository distributionSetRepository, - final TargetRepository targetRepository, final ActionStatusRepository actionStatusRepository, - final AuditorAware auditorProvider, final EventPublisherHolder eventPublisherHolder, - final AfterTransactionCommitExecutor afterCommit, final VirtualPropertyReplacer virtualPropertyReplacer, - final PlatformTransactionManager txManager, + final DistributionSetManagement distributionSetManagement, final TargetRepository targetRepository, + final ActionStatusRepository actionStatusRepository, final AuditorAware auditorProvider, + final EventPublisherHolder eventPublisherHolder, final AfterTransactionCommitExecutor afterCommit, + final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager, final TenantConfigurationManagement tenantConfigurationManagement, final QuotaManagement quotaManagement, final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware, final JpaProperties properties, final RepositoryProperties repositoryProperties) { - return new JpaDeploymentManagement(entityManager, actionRepository, distributionSetRepository, targetRepository, - actionStatusRepository, auditorProvider, eventPublisherHolder, afterCommit, virtualPropertyReplacer, - txManager, tenantConfigurationManagement, quotaManagement, systemSecurityContext, tenantAware, - properties.getDatabase(), repositoryProperties); + return new JpaDeploymentManagement(entityManager, actionRepository, distributionSetManagement, + distributionSetRepository, targetRepository, actionStatusRepository, auditorProvider, + eventPublisherHolder, afterCommit, virtualPropertyReplacer, txManager, tenantConfigurationManagement, + quotaManagement, systemSecurityContext, tenantAware, properties.getDatabase(), repositoryProperties); } /** @@ -813,7 +816,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link AutoAssignScheduler} bean. - * + * * Note: does not activate in test profile, otherwise it is hard to test the * auto assign functionality. * @@ -843,12 +846,12 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link AutoActionCleanup} bean. - * + * * @param deploymentManagement * Deployment management service * @param configManagement * Tenant configuration service - * + * * @return a new {@link AutoActionCleanup} bean */ @Bean @@ -859,7 +862,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link AutoCleanupScheduler} bean. - * + * * @param systemManagement * to find all tenants * @param systemSecurityContext @@ -868,7 +871,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { * to lock the tenant for auto assignment * @param cleanupTasks * a list of cleanup tasks - * + * * @return a new {@link AutoCleanupScheduler} bean */ @Bean @@ -883,10 +886,10 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link RolloutScheduler} bean. - * + * * Note: does not activate in test profile, otherwise it is hard to test the * rollout handling functionality. - * + * * @param systemManagement * to find all tenants * @param rolloutManagement @@ -906,7 +909,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * Creates the {@link RsqlVisitorFactory} bean. - * + * * @return A new {@link RsqlVisitorFactory} bean. */ @Bean @@ -917,7 +920,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * Obtains the {@link RsqlVisitorFactoryHolder} bean. - * + * * @return The {@link RsqlVisitorFactoryHolder} singleton. */ @Bean @@ -925,4 +928,22 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { return RsqlVisitorFactoryHolder.getInstance(); } + /** + * {@link JpaDistributionSetInvalidationManagement} bean. + * + * @return a new {@link JpaDistributionSetInvalidationManagement} + */ + @Bean + @ConditionalOnMissingBean + JpaDistributionSetInvalidationManagement distributionSetInvalidationManagement( + final DistributionSetManagement distributionSetManagement, final RolloutManagement rolloutManagement, + final DeploymentManagement deploymentManagement, + final TargetFilterQueryManagement targetFilterQueryManagement, final PlatformTransactionManager txManager, + final RepositoryProperties repositoryProperties, final TenantAware tenantAware, + final LockRegistry lockRegistry) { + return new JpaDistributionSetInvalidationManagement(distributionSetManagement, rolloutManagement, + deploymentManagement, targetFilterQueryManagement, txManager, repositoryProperties, tenantAware, + lockRegistry); + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutGroupRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutGroupRepository.java index 1555eb0f2..ac08e3707 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutGroupRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutGroupRepository.java @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.repository.jpa; +import java.util.Collection; import java.util.List; import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; @@ -33,7 +34,7 @@ public interface RolloutGroupRepository /** * Retrieves all {@link RolloutGroup} referring a specific rollout in the * order of creating them. ID ASC. - * + * * @param rollout * the rollout the rolloutgroups belong to * @return the rollout groups belonging to a rollout ordered by ID ASC. @@ -43,7 +44,7 @@ public interface RolloutGroupRepository /** * Retrieves all {@link RolloutGroup} referring a specific rollout in a * specific {@link RolloutGroupStatus}. - * + * * @param rollout * the rollout the rolloutgroup belong to * @param status @@ -58,7 +59,7 @@ public interface RolloutGroupRepository * the spring-data, so this is specific usecase regarding to the * rollout-management to find out rolloutgroups which are in specific * states. - * + * * @param rolloutId * the ID of the rollout the rolloutgroup belong to * @param rolloutGroupStatus1 @@ -74,10 +75,10 @@ public interface RolloutGroupRepository @Param("status2") RolloutGroupStatus rolloutGroupStatus2); /** - * + * * Counts all rollout-groups refering to a given {@link Rollout} by its ID * and groups which not having the given status. - * + * * @param rolloutId * the ID of the rollout refering the groups * @param status1 @@ -97,7 +98,7 @@ public interface RolloutGroupRepository /** * Retrieves all {@link RolloutGroup} for a specific parent in a specific * status. Retrieves the child rolloutgroup for a specific status. - * + * * @param rolloutGroupId * the rolloutgroupId to find the parents * @param status @@ -111,7 +112,7 @@ public interface RolloutGroupRepository /** * Updates all {@link RolloutGroup#getStatus()} of children for given * parent. - * + * * @param parent * the parent rolloutgroup * @param status @@ -125,7 +126,7 @@ public interface RolloutGroupRepository /** * Retrieves all {@link RolloutGroup} for a specific rollout and status not * having ordered by ID DESC, latest top. - * + * * @param rollout * the rollout the rolloutgroup belong to * @param notStatus @@ -135,9 +136,22 @@ public interface RolloutGroupRepository */ List findByRolloutAndStatusNotOrderByIdDesc(JpaRollout rollout, RolloutGroupStatus notStatus); + /** + * Retrieves all {@link RolloutGroup}s for a specific rollout and status not + * having. + * + * @param rollout + * the rollout the rolloutgroup belong to + * @param status + * the status which the rolloutgroup should not have + * @return rolloutgroup referring to a rollout and not having a specific + * status. + */ + List findByRolloutAndStatusNotIn(JpaRollout rollout, Collection status); + /** * Retrieves all {@link RolloutGroup} for a specific rollout. - * + * * @param rolloutId * the ID of the rollout to find the rollout groups * @param page @@ -148,10 +162,10 @@ public interface RolloutGroupRepository /** * Counts all {@link RolloutGroup} for a specific rollout. - * + * * @param rolloutId * the ID of the rollout to find the rollout groups - * + * * @return the amount of found {@link RolloutGroup}s. */ long countByRolloutId(Long rolloutId); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutRepository.java index 1356225d4..b6f4f022b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutRepository.java @@ -15,6 +15,7 @@ import java.util.Optional; import javax.persistence.EntityManager; import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; +import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity; @@ -33,7 +34,7 @@ public interface RolloutRepository /** * Retrieves all {@link Rollout} for given status. - * + * * @param status * the status of the rollouts to find * @return the list of {@link Rollout} for specific status @@ -44,7 +45,7 @@ public interface RolloutRepository /** * Retrieves all {@link Rollout} for a specific {@code name} - * + * * @param name * the rollout name * @return {@link Rollout} for specific name @@ -64,4 +65,28 @@ public interface RolloutRepository @Transactional @Query("DELETE FROM JpaRollout t WHERE t.tenant = :tenant") void deleteByTenant(@Param("tenant") String tenant); + + /** + * Retrieves all {@link Rollout}s for a specific {@link DistributionSet} in + * a given {@link RolloutStatus}. + * + * @param set + * the distribution set + * @param status + * the status of the rollout + * @return {@link Rollout} for specific distribution set + */ + List findByDistributionSetAndStatusIn(DistributionSet set, Collection status); + + /** + * Counts all {@link Rollout}s for a specific {@link DistributionSet} in a + * given {@link RolloutStatus}. + * + * @param distributionSetId + * the distribution set + * @param status + * the status of the rollout + * @return the count + */ + long countByDistributionSetIdAndStatusIn(long distributionSetId, Collection status); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryRepository.java index cc3d70895..d5170e535 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryRepository.java @@ -32,7 +32,7 @@ public interface TargetFilterQueryRepository /** * Find customer target filter by name - * + * * @param name * @return custom target filter */ @@ -56,6 +56,16 @@ public interface TargetFilterQueryRepository @Query("update JpaTargetFilterQuery d set d.autoAssignDistributionSet = NULL, d.autoAssignActionType = NULL where d.autoAssignDistributionSet in :ids") void unsetAutoAssignDistributionSetAndActionType(@Param("ids") Long... dsIds); + /** + * Counts all target filters that have a given auto assign distribution set + * assigned. + * + * @param autoAssignDistributionSetId + * the id of the distribution set + * @return the count + */ + long countByAutoAssignDistributionSetId(long autoAssignDistributionSetId); + /** * Deletes all {@link TenantAwareBaseEntity} of a given tenant. For safety * reasons (this is a "delete everything" query after all) we add the tenant diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutCreate.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutCreate.java index 9011b5026..faa1b0855 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutCreate.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutCreate.java @@ -11,9 +11,7 @@ package org.eclipse.hawkbit.repository.jpa.builder; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.builder.AbstractRolloutUpdateCreate; import org.eclipse.hawkbit.repository.builder.RolloutCreate; -import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; -import org.eclipse.hawkbit.repository.model.DistributionSet; public class JpaRolloutCreate extends AbstractRolloutUpdateCreate implements RolloutCreate { private final DistributionSetManagement distributionSetManagement; @@ -28,7 +26,7 @@ public class JpaRolloutCreate extends AbstractRolloutUpdateCreate rollout.setName(name); rollout.setDescription(description); - rollout.setDistributionSet(findDistributionSetAndThrowExceptionIfNotFound(set)); + rollout.setDistributionSet(distributionSetManagement.getValidAndComplete(set)); rollout.setTargetFilterQuery(targetFilterQuery); rollout.setStartAt(startAt); rollout.setWeight(weight); @@ -43,9 +41,4 @@ public class JpaRolloutCreate extends AbstractRolloutUpdateCreate return rollout; } - - private DistributionSet findDistributionSetAndThrowExceptionIfNotFound(final Long setId) { - return distributionSetManagement.get(setId) - .orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, setId)); - } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetFilterQueryCreate.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetFilterQueryCreate.java index f73b498ff..aee08ff99 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetFilterQueryCreate.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetFilterQueryCreate.java @@ -11,11 +11,9 @@ package org.eclipse.hawkbit.repository.jpa.builder; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.builder.AbstractTargetFilterQueryUpdateCreate; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryCreate; -import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetFilterQuery; import org.eclipse.hawkbit.repository.model.Action.ActionType; -import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; /** @@ -35,16 +33,11 @@ public class JpaTargetFilterQueryCreate extends AbstractTargetFilterQueryUpdateC public JpaTargetFilterQuery build() { return new JpaTargetFilterQuery(name, query, - getAutoAssignDistributionSetId().map(this::findDistributionSetAndThrowExceptionIfNotFound).orElse(null), + getAutoAssignDistributionSetId().map(distributionSetManagement::getValidAndComplete).orElse(null), getAutoAssignActionType().filter(JpaTargetFilterQueryCreate::isAutoAssignActionTypeValid).orElse(null), weight); } - private DistributionSet findDistributionSetAndThrowExceptionIfNotFound(final Long setId) { - return distributionSetManagement.get(setId) - .orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, setId)); - } - private static boolean isAutoAssignActionTypeValid(final ActionType actionType) { if (!TargetFilterQuery.ALLOWED_AUTO_ASSIGN_ACTION_TYPES.contains(actionType)) { throw new InvalidAutoAssignActionTypeException(); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSet.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSet.java index 16349fea4..8526451b5 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSet.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSet.java @@ -116,6 +116,9 @@ public class JpaDistributionSet extends AbstractJpaNamedVersionedEntity implemen @Column(name = "complete") private boolean complete; + @Column(name = "valid") + private boolean valid; + /** * Default constructor. */ @@ -145,6 +148,7 @@ public class JpaDistributionSet extends AbstractJpaNamedVersionedEntity implemen super(name, version, description); this.requiredMigrationStep = requiredMigrationStep; + this.valid = true; this.type = type; if (moduleList != null) { moduleList.forEach(this::addModule); @@ -327,6 +331,15 @@ public class JpaDistributionSet extends AbstractJpaNamedVersionedEntity implemen return complete; } + @Override + public boolean isValid() { + return valid; + } + + public void invalidate() { + this.valid = false; + } + @Override public void fireCreateEvent(final DescriptorEvent descriptorEvent) { publishEventWithEventPublisher( diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java index ecf156e16..a487ed55a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java @@ -93,7 +93,8 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event @ConversionValue(objectValue = "DELETING", dataValue = "9"), @ConversionValue(objectValue = "DELETED", dataValue = "10"), @ConversionValue(objectValue = "WAITING_FOR_APPROVAL", dataValue = "11"), - @ConversionValue(objectValue = "APPROVAL_DENIED", dataValue = "12") }) + @ConversionValue(objectValue = "APPROVAL_DENIED", dataValue = "12"), + @ConversionValue(objectValue = "STOPPING", dataValue = "13") }) @Convert("rolloutstatus") @NotNull private RolloutStatus status = RolloutStatus.CREATING; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetSpecification.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetSpecification.java index 953b304f4..8a2fe163b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetSpecification.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetSpecification.java @@ -45,7 +45,7 @@ public final class DistributionSetSpecification { /** * {@link Specification} for retrieving {@link DistributionSet}s by its * DELETED attribute. - * + * * @param isDeleted * TRUE/FALSE are compared to the attribute DELETED. If NULL the * attribute is ignored @@ -59,7 +59,7 @@ public final class DistributionSetSpecification { /** * {@link Specification} for retrieving {@link DistributionSet}s by its * COMPLETED attribute. - * + * * @param isCompleted * TRUE/FALSE are compared to the attribute COMPLETED. If NULL * the attribute is ignored @@ -70,6 +70,20 @@ public final class DistributionSetSpecification { } + /** + * {@link Specification} for retrieving {@link DistributionSet}s by its + * VALID attribute. + * + * @param isValid + * TRUE/FALSE are compared to the attribute VALID. If NULL the + * attribute is ignored + * @return the {@link DistributionSet} {@link Specification} + */ + public static Specification isValid(final Boolean isValid) { + return (targetRoot, query, cb) -> cb.equal(targetRoot. get(JpaDistributionSet_.valid), isValid); + + } + /** * {@link Specification} for retrieving {@link DistributionSet} with given * {@link DistributionSet#getId()}. @@ -111,7 +125,7 @@ public final class DistributionSetSpecification { /** * {@link Specification} for retrieving {@link DistributionSet}s by "like * name or like description or like version". - * + * * @param subString * to be filtered on * @return the {@link DistributionSet} {@link Specification} @@ -126,7 +140,7 @@ public final class DistributionSetSpecification { /** * {@link Specification} for retrieving {@link DistributionSet}s by "like * name and like version". - * + * * @param name * to be filtered on * @param version @@ -142,7 +156,7 @@ public final class DistributionSetSpecification { /** * {@link Specification} for retrieving {@link DistributionSet}s by "has at * least one of the given tag names". - * + * * @param tagNames * to be filtered on * @param selectDSWithNoTag @@ -187,7 +201,7 @@ public final class DistributionSetSpecification { /** * returns query criteria {@link Specification} comparing case insensitive * "NAME == AND VERSION ==". - * + * * @param name * to be filtered on * @param version diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_19__add_valid_flag_to_ds___DB2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_19__add_valid_flag_to_ds___DB2.sql new file mode 100644 index 000000000..3ab0e7502 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_19__add_valid_flag_to_ds___DB2.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_distribution_set ADD COLUMN valid BOOLEAN; + +UPDATE sp_distribution_set SET valid = 1; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_19__add_valid_flag_to_ds___H2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_19__add_valid_flag_to_ds___H2.sql new file mode 100644 index 000000000..3ab0e7502 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_19__add_valid_flag_to_ds___H2.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_distribution_set ADD COLUMN valid BOOLEAN; + +UPDATE sp_distribution_set SET valid = 1; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_19__add_valid_flag_to_ds___MYSQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_19__add_valid_flag_to_ds___MYSQL.sql new file mode 100644 index 000000000..3ab0e7502 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_19__add_valid_flag_to_ds___MYSQL.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_distribution_set ADD COLUMN valid BOOLEAN; + +UPDATE sp_distribution_set SET valid = 1; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_19__add_valid_flag_to_ds___POSTGRESQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_19__add_valid_flag_to_ds___POSTGRESQL.sql new file mode 100644 index 000000000..3ab0e7502 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_19__add_valid_flag_to_ds___POSTGRESQL.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_distribution_set ADD COLUMN valid BOOLEAN; + +UPDATE sp_distribution_set SET valid = 1; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_19__add_valid_flag_to_ds___SQL_SERVER.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_19__add_valid_flag_to_ds___SQL_SERVER.sql new file mode 100644 index 000000000..3ab0e7502 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_19__add_valid_flag_to_ds___SQL_SERVER.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_distribution_set ADD COLUMN valid BOOLEAN; + +UPDATE sp_distribution_set SET valid = 1; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/RolloutEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/RolloutEventTest.java index 0aedf8190..7bf53c0fa 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/RolloutEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/RolloutEventTest.java @@ -8,10 +8,13 @@ */ package org.eclipse.hawkbit.repository.event.remote.entity; +import java.util.Collections; + import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCondition; import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder; +import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.junit.jupiter.api.Test; import io.qameta.allure.Description; @@ -34,8 +37,11 @@ public class RolloutEventTest extends AbstractRemoteEntityEventTest { @Override protected Rollout createEntity() { testdataFactory.createTarget("12345"); - final DistributionSet ds = distributionSetManagement.create(entityFactory.distributionSet() - .create().name("incomplete").version("2").description("incomplete").type("os")); + final SoftwareModule module = softwareModuleManagement.create( + entityFactory.softwareModule().create().name("swm").version("2").description("desc").type("os")); + final DistributionSet ds = distributionSetManagement + .create(entityFactory.distributionSet().create().name("complete").version("2").description("complete") + .type("os").modules(Collections.singletonList(module.getId()))); return rolloutManagement.create( entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds), diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/RolloutGroupEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/RolloutGroupEventTest.java index a1453a608..ce8f983ad 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/RolloutGroupEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/RolloutGroupEventTest.java @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.repository.event.remote.entity; import static org.assertj.core.api.Assertions.assertThat; +import java.util.Collections; import java.util.UUID; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -17,6 +18,7 @@ import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCondition; import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder; +import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.junit.jupiter.api.Test; import io.qameta.allure.Description; @@ -75,9 +77,11 @@ public class RolloutGroupEventTest extends AbstractRemoteEntityEventTest { + tenantAware.runAsTenant(tenant, () -> systemSecurityContext.runAsSystem(() -> { + rolloutManagement.handleRollouts(); + return 0; + })); + }); + handleRolloutsThread.start(); + // wait until at least one RolloutGroup is created, as this means that + // the thread has started and has acquired the lock + Awaitility.await().until(() -> tenantAware.runAsTenant(tenant, () -> systemSecurityContext + .runAsSystem(() -> rolloutGroupManagement.findByRollout(PAGE, rollout.getId()).getSize() > 0))); + + assertThatExceptionOfType(StopRolloutException.class) + .as("Invalidation of distributionSet should throw an exception") + .isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet( + new DistributionSetInvalidation(Collections.singletonList(distributionSet.getId()), + CancelationType.SOFT, true))); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DeploymentManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DeploymentManagementTest.java index 23f8c2971..8b0563fdb 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DeploymentManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DeploymentManagementTest.java @@ -43,6 +43,7 @@ import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.ForceQuitActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; @@ -844,8 +845,8 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { /** * test a simple deployment by calling the - * {@link TargetRepository#assignDistributionSet(DistributionSet, Iterable)} and - * checking the active action and the action history of the targets. + * {@link TargetRepository#assignDistributionSet(DistributionSet, Iterable)} + * and checking the active action and the action history of the targets. * */ @Test @@ -1088,9 +1089,10 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { } /** - * test the deletion of {@link DistributionSet}s including exception in case of - * {@link Target}s are assigned by {@link Target#getAssignedDistributionSet()} - * or {@link Target#getInstalledDistributionSet()} + * test the deletion of {@link DistributionSet}s including exception in case + * of {@link Target}s are assigned by + * {@link Target#getAssignedDistributionSet()} or + * {@link Target#getInstalledDistributionSet()} */ @Test @Description("Deletes distribution set. Expected behaviour is that a soft delete is performed " @@ -1357,12 +1359,36 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { assertThat(distributionSetRepository.findAll()).hasSize(1); } + @Test + @Description("Tests that an exception is thrown when a target is assigned to an incomplete distribution set") + public void verifyAssignTargetsToIncompleteDistribution() { + final DistributionSet distributionSet = testdataFactory.createIncompleteDistributionSet(); + final Target target = testdataFactory.createTarget(); + + assertThatExceptionOfType(IncompleteDistributionSetException.class) + .as("Incomplete distributionSet should throw an exception") + .isThrownBy(() -> assignDistributionSet(distributionSet, target)); + + } + + @Test + @Description("Tests that an exception is thrown when a target is assigned to an invalidated distribution set") + public void verifyAssignTargetsToInvalidDistribution() { + final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet(); + final Target target = testdataFactory.createTarget(); + + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception") + .isThrownBy(() -> assignDistributionSet(distributionSet, target)); + + } + /** * Helper methods that creates 2 lists of targets and a list of distribution * sets. *

- * All created distribution sets are assigned to all targets of the target - * list deployedTargets. + * All created distribution sets are assigned to all targets of the + * target list deployedTargets. * * @param undeployedTargetPrefix * prefix to be used as target controller prefix @@ -1371,7 +1397,8 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { * @param deployedTargetPrefix * prefix to be used as target controller prefix * @param noOfDeployedTargets - * number of targets to which the created distribution sets assigned + * number of targets to which the created distribution sets + * assigned * @param noOfDistributionSets * number of distribution sets * @param distributionSetPrefix diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetInvalidationManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetInvalidationManagementTest.java new file mode 100644 index 000000000..aac9c2356 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetInvalidationManagementTest.java @@ -0,0 +1,304 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.util.Collections; +import java.util.List; + +import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; +import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidationCount; +import org.eclipse.hawkbit.repository.model.Rollout; +import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; +import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.repository.test.util.WithUser; +import org.junit.jupiter.api.Test; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; + +/** + * Test class testing the functionality of invalidating a + * {@link DistributionSet} + * + */ +@Feature("Component Tests - Repository") +@Story("Distribution set invalidation management") +class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTest { + + @Test + @Description("Verify invalidation of distribution sets that only removes distribution sets from auto assignments") + void verifyInvalidateDistributionSetStopAutoAssignment() { + final InvalidationTestData invalidationTestData = createInvalidationTestData( + "verifyInvalidateDistributionSetStopAutoAssignment"); + + final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE, + false); + final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement + .countEntitiesForInvalidation(distributionSetInvalidation); + assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 0, 0); + + distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation); + rolloutManagement.handleRollouts(); + + assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get() + .getAutoAssignDistributionSet()).isNull(); + assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus()) + .isNotIn(RolloutStatus.STOPPING, RolloutStatus.FINISHED); + for (final Target target : invalidationTestData.getTargets()) { + // if status is pending, the assignment has not been canceled + assertThat(targetRepository.findById(target.getId()).get().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + assertThat(actionRepository.findByTarget(target).size()).isEqualTo(1); + assertThat(actionRepository.findByTarget(target).get(0).getStatus()).isEqualTo(Status.RUNNING); + } + } + + @Test + @Description("Verify invalidation of distribution sets that removes distribution sets from auto assignments and stops rollouts") + void verifyInvalidateDistributionSetStopRollouts() { + final InvalidationTestData invalidationTestData = createInvalidationTestData( + "verifyInvalidateDistributionSetStopRollouts"); + + final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE, + true); + final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement + .countEntitiesForInvalidation(distributionSetInvalidation); + assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 0, 1); + + distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation); + rolloutManagement.handleRollouts(); + + assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get() + .getAutoAssignDistributionSet()).isNull(); + assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus()) + .isEqualTo(RolloutStatus.FINISHED); + assertNoScheduledActionsExist(invalidationTestData.getRollout()); + assertRolloutGroupsAreFinished(invalidationTestData.getRollout()); + for (final Target target : invalidationTestData.getTargets()) { + // if status is pending, the assignment has not been canceled + assertThat( + targetRepository.findById(invalidationTestData.getTargets().get(0).getId()).get().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + assertThat(actionRepository.findByTarget(target).size()).isEqualTo(1); + assertThat(actionRepository.findByTarget(target).get(0).getStatus()).isEqualTo(Status.RUNNING); + } + } + + @Test + @Description("Verify invalidation of distribution sets that removes distribution sets from auto assignments, stops rollouts and force cancels assignments") + void verifyInvalidateDistributionSetStopAllAndForceCancel() { + final InvalidationTestData invalidationTestData = createInvalidationTestData( + "verifyInvalidateDistributionSetStopAllAndForceCancel"); + + final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.FORCE, + true); + final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement + .countEntitiesForInvalidation(distributionSetInvalidation); + assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 5, 1); + + distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation); + rolloutManagement.handleRollouts(); + + assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get() + .getAutoAssignDistributionSet()).isNull(); + assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus()) + .isEqualTo(RolloutStatus.FINISHED); + assertNoScheduledActionsExist(invalidationTestData.getRollout()); + assertRolloutGroupsAreFinished(invalidationTestData.getRollout()); + for (final Target target : invalidationTestData.getTargets()) { + assertThat(targetRepository.findById(target.getId()).get().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.IN_SYNC); + assertThat(actionRepository.findByTarget(target).size()).isEqualTo(1); + assertThat(actionRepository.findByTarget(target).get(0).getStatus()).isEqualTo(Status.CANCELED); + } + } + + private void assertNoScheduledActionsExist(final Rollout rollout) { + assertThat( + actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), Status.SCHEDULED).getTotalElements()) + .isZero(); + } + + private void assertRolloutGroupsAreFinished(final Rollout rollout) { + assertThat(rolloutGroupRepository.findByRolloutId(rollout.getId(), PAGE)) + .allMatch(rolloutGroup -> rolloutGroup.getStatus().equals(RolloutGroupStatus.FINISHED)); + } + + @Test + @Description("Verify invalidation of distribution sets that removes distribution sets from auto assignments, stops rollouts and cancels assignments") + void verifyInvalidateDistributionSetStopAll() { + final InvalidationTestData invalidationTestData = createInvalidationTestData( + "verifyInvalidateDistributionSetStopAll"); + + final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation( + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.SOFT, + true); + final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement + .countEntitiesForInvalidation(distributionSetInvalidation); + assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 5, 1); + + distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation); + + assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get() + .getAutoAssignDistributionSet()).isNull(); + assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus()) + .isIn(RolloutStatus.STOPPING, RolloutStatus.FINISHED); + for (final Target target : invalidationTestData.getTargets()) { + assertThat(targetRepository.findById(target.getId()).get().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + assertThat(actionRepository.findByTarget(target).size()).isEqualTo(1); + assertThat(actionRepository.findByTarget(target).get(0).getStatus()).isEqualTo(Status.CANCELING); + } + } + + @Test + @Description("Verify that invalidating an incomplete distribution set throws an exception") + void verifyInvalidateIncompleteDistributionSetThrowsException() { + final DistributionSet distributionSet = testdataFactory.createIncompleteDistributionSet(); + + assertThatExceptionOfType(IncompleteDistributionSetException.class) + .as("Incomplete distributionSet should throw an exception") + .isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet( + new DistributionSetInvalidation(Collections.singletonList(distributionSet.getId()), + CancelationType.SOFT, true))); + } + + @Test + @Description("Verify that invalidating an invalidated distribution set throws an exception") + void verifyInvalidateInvalidatedDistributionSetThrowsException() { + final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet(); + + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception") + .isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet( + new DistributionSetInvalidation(Collections.singletonList(distributionSet.getId()), + CancelationType.SOFT, true))); + } + + @Test + @Description("Verify that a user that has authority READ_REPOSITORY and UPDATE_REPOSITORY is not allowed to invalidate a distribution set") + @WithUser(authorities = { "READ_REPOSITORY", "UPDATE_REPOSITORY" }) + void verifyInvalidateWithReadAndUpdateRepoAuthority() { + final InvalidationTestData invalidationTestData = systemSecurityContext + .runAsSystem(() -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAuthority")); + + assertThatExceptionOfType(InsufficientPermissionException.class) + .as("Insufficient permission exception expected") + .isThrownBy(() -> distributionSetInvalidationManagement + .invalidateDistributionSet(new DistributionSetInvalidation( + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), + CancelationType.NONE, false))); + } + + @Test + @Description("Verify that a user that has authority READ_REPOSITORY, UPDATE_REPOSITORY and UPDATE_TARGET is allowed to invalidate a distribution set only without canceling rollouts") + @WithUser(authorities = { "READ_REPOSITORY", "UPDATE_REPOSITORY", "UPDATE_TARGET" }) + void verifyInvalidateWithReadAndUpdateRepoAndUpdateTargetAuthority() { + final InvalidationTestData invalidationTestData = systemSecurityContext.runAsSystem( + () -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAndUpdateTargetAuthority")); + + assertThatExceptionOfType(InsufficientPermissionException.class) + .as("Insufficient permission exception expected") + .isThrownBy(() -> distributionSetInvalidationManagement + .invalidateDistributionSet(new DistributionSetInvalidation( + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), + CancelationType.SOFT, true))); + + distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation( + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE, + false)); + assertThat( + distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid()) + .isFalse(); + } + + @Test + @Description("Verify that a user that has authority READ_REPOSITORY, UPDATE_REPOSITORY, UPDATE_ROLLOUT and UPDATE_TARGET is allowed to invalidate a distribution") + @WithUser(authorities = { "READ_REPOSITORY", "UPDATE_REPOSITORY", "UPDATE_TARGET", "UPDATE_ROLLOUT" }) + void verifyInvalidateWithReadAndUpdateRepoAndUpdateTargetAndUpdateRolloutAuthority() { + final InvalidationTestData invalidationTestData = systemSecurityContext.runAsSystem( + () -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAndUpdateTargetAuthority")); + + distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation( + Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.SOFT, + true)); + assertThat( + distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid()) + .isFalse(); + } + + private InvalidationTestData createInvalidationTestData(final String testName) { + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); + final List targets = testdataFactory.createTargets(5, testName); + assignDistributionSet(distributionSet, targets); + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create(entityFactory.targetFilterQuery() + .create().name(testName).query("name==*").autoAssignDistributionSet(distributionSet)); + final Rollout rollout = testdataFactory.createRolloutByVariables(testName, "desc", 2, "name==*", + distributionSet, "50", "80"); + + return new InvalidationTestData(distributionSet, targets, targetFilterQuery, rollout); + } + + private static class InvalidationTestData { + private final DistributionSet distributionSet; + private final List targets; + private final TargetFilterQuery targetFilterQuery; + private final Rollout rollout; + + public InvalidationTestData(final DistributionSet distributionSet, final List targets, + final TargetFilterQuery targetFilterQuery, final Rollout rollout) { + super(); + this.distributionSet = distributionSet; + this.targets = targets; + this.targetFilterQuery = targetFilterQuery; + this.rollout = rollout; + } + + public DistributionSet getDistributionSet() { + return distributionSet; + } + + public List getTargets() { + return targets; + } + + public TargetFilterQuery getTargetFilterQuery() { + return targetFilterQuery; + } + + public Rollout getRollout() { + return rollout; + } + } + + private void assertDistributionSetInvalidationCount( + final DistributionSetInvalidationCount distributionSetInvalidationCount, + final long expectedAutoAssignmentCount, final long expectedActionCount, final long expectedRolloutCount) { + assertThat(distributionSetInvalidationCount.getAutoAssignmentCount()).isEqualTo(expectedAutoAssignmentCount); + assertThat(distributionSetInvalidationCount.getActionCount()).isEqualTo(expectedActionCount); + assertThat(distributionSetInvalidationCount.getRolloutsCount()).isEqualTo(expectedRolloutCount); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetManagementTest.java index 477d6141d..fb0523c02 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetManagementTest.java @@ -31,16 +31,20 @@ import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreated import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetTagCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; +import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; -import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; +import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThisDistributionSetException; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetMetadata; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetFilter.DistributionSetFilterBuilder; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; import org.eclipse.hawkbit.repository.model.DistributionSetTag; import org.eclipse.hawkbit.repository.model.DistributionSetType; @@ -174,6 +178,13 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { verifyThrownExceptionBy(() -> distributionSetManagement.updateMetaData(set.getId(), entityFactory.generateDsMetadata(NOT_EXIST_ID, "xxx")), "DistributionSetMetadata"); + + verifyThrownExceptionBy(() -> distributionSetManagement.getOrElseThrowException(NOT_EXIST_IDL), + "DistributionSet"); + + verifyThrownExceptionBy(() -> distributionSetManagement.getValidAndComplete(NOT_EXIST_IDL), "DistributionSet"); + + verifyThrownExceptionBy(() -> distributionSetManagement.getValid(NOT_EXIST_IDL), "DistributionSet"); } @Test @@ -493,6 +504,16 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { assertThat(ds.isRequiredMigrationStep()).isTrue(); } + @Test + @Description("Verifies that an exception is thrown when trying to update an invalid distribution set") + public void updateInvalidDistributionSet() { + final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet(); + + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception").isThrownBy(() -> distributionSetManagement + .update(entityFactory.distributionSet().update(distributionSet.getId()).name("new_name"))); + } + @Test @Description("Verifies the enforcement of the software module quota per distribution set.") public void assignSoftwareModulesUntilQuotaIsExceeded() { @@ -533,6 +554,33 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { } + @Test + @Description("Verifies that an exception is thrown when trying to assign software modules to an invalidated distribution set.") + public void verifyAssignSoftwareModulesToInvalidDistributionSet() { + final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet(); + final SoftwareModule softwareModule = testdataFactory.createSoftwareModuleOs(); + + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception") + .isThrownBy(() -> distributionSetManagement.assignSoftwareModules(distributionSet.getId(), + Collections.singletonList(softwareModule.getId()))); + } + + @Test + @Description("Verifies that an exception is thrown when trying to unassign a software module from an invalidated distribution set.") + public void verifyUnassignSoftwareModulesToInvalidDistributionSet() { + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); + final SoftwareModule softwareModule = testdataFactory.createSoftwareModuleOs(); + distributionSetManagement.assignSoftwareModules(distributionSet.getId(), + Collections.singletonList(softwareModule.getId())); + distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation( + Collections.singletonList(distributionSet.getId()), CancelationType.NONE, false)); + + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception").isThrownBy(() -> distributionSetManagement + .unassignSoftwareModule(distributionSet.getId(), softwareModule.getId())); + } + @Test @WithUser(allSpPermissions = true) @Description("Checks that metadata for a distribution set can be updated.") @@ -937,6 +985,24 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { assertThat(distributionSetManagement.findByCompleted(PAGE, true).getTotalElements()).isEqualTo(1); } + @Test + @Description("Deletes an invalid distribution set") + public void deleteInvalidDistributionSet() { + final DistributionSet set = testdataFactory.createAndInvalidateDistributionSet(); + assertThat(distributionSetRepository.findById(set.getId())).isNotEmpty(); + distributionSetManagement.delete(set.getId()); + assertThat(distributionSetRepository.findById(set.getId())).isEmpty(); + } + + @Test + @Description("Deletes an incomplete distribution set") + public void deleteIncompleteDistributionSet() { + final DistributionSet set = testdataFactory.createIncompleteDistributionSet(); + assertThat(distributionSetRepository.findById(set.getId())).isNotEmpty(); + distributionSetManagement.delete(set.getId()); + assertThat(distributionSetRepository.findById(set.getId())).isEmpty(); + } + @Test @Description("Queries and loads the metadata related to a given software module.") public void findAllDistributionSetMetadataByDsId() { @@ -1013,4 +1079,54 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { assertThat(collect).containsAll(searchIds); } + @Test + @Description("Verify that an exception is thrown when trying to get an invalid distribution set") + public void verifyGetValid() { + final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet(); + + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception") + .isThrownBy(() -> distributionSetManagement.getValid(distributionSet.getId())); + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception") + .isThrownBy(() -> distributionSetManagement.getValidAndComplete(distributionSet.getId())); + } + + @Test + @Description("Verify that an exception is thrown when trying to get an incomplete distribution set") + public void verifyGetValidAndComplete() { + final DistributionSet distributionSet = testdataFactory.createIncompleteDistributionSet(); + + assertThatExceptionOfType(IncompleteDistributionSetException.class) + .as("Incomplete distributionSet should throw an exception") + .isThrownBy(() -> distributionSetManagement.getValidAndComplete(distributionSet.getId())); + } + + @Test + @Description("Verify that an exception is thrown when trying to create or update metadata for an invalid distribution set.") + public void createMetadataForInvalidDistributionSet() { + final String knownKey1 = "myKnownKey1"; + final String knownKey2 = "myKnownKey2"; + final String knownValue = "myKnownValue"; + final String knownUpdateValue = "knownUpdateValue"; + + final DistributionSet ds = testdataFactory.createDistributionSet(); + distributionSetManagement.createMetaData(ds.getId(), + Collections.singletonList(entityFactory.generateDsMetadata(knownKey1, knownValue))); + + distributionSetInvalidationManagement.invalidateDistributionSet( + new DistributionSetInvalidation(Arrays.asList(ds.getId()), CancelationType.NONE, false)); + + // assert that no new metadata can be created + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception") + .isThrownBy(() -> distributionSetManagement.createMetaData(ds.getId(), + Collections.singletonList(entityFactory.generateDsMetadata(knownKey2, knownValue)))); + + // assert that an existing metadata can not be updated + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception").isThrownBy(() -> distributionSetManagement + .updateMetaData(ds.getId(), entityFactory.generateDsMetadata(knownKey1, knownUpdateValue))); + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java index 8872e2fc8..9344bf198 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java @@ -46,6 +46,8 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; @@ -72,7 +74,6 @@ import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus; import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Page; @@ -243,8 +244,9 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { assertThat(firstGroup.getStatus()).isEqualTo(RolloutGroupStatus.RUNNING); // verify other groups are scheduled - final List scheduledGroups = rolloutGroupManagement.findByRollout( - new OffsetBasedPageRequest(1, 100, Sort.by(Direction.ASC, "id")), createdRollout.getId()).getContent(); + final List scheduledGroups = rolloutGroupManagement + .findByRollout(new OffsetBasedPageRequest(1, 100, Sort.by(Direction.ASC, "id")), createdRollout.getId()) + .getContent(); scheduledGroups.forEach(group -> assertThat(group.getStatus()).isEqualTo(RolloutGroupStatus.SCHEDULED) .as("group which should be in scheduled state is in " + group.getStatus() + " state")); // verify that the first group actions has been started and are in state @@ -436,8 +438,9 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { assertThat(errorGroup.get(0).getStatus()).isEqualTo(RolloutGroupStatus.ERROR); // all other groups should still be in scheduled state - final List scheduleGroups = rolloutGroupManagement.findByRollout( - new OffsetBasedPageRequest(1, 100, Sort.by(Direction.ASC, "id")), createdRollout.getId()).getContent(); + final List scheduleGroups = rolloutGroupManagement + .findByRollout(new OffsetBasedPageRequest(1, 100, Sort.by(Direction.ASC, "id")), createdRollout.getId()) + .getContent(); scheduleGroups.forEach(group -> assertThat(group.getStatus()).isEqualTo(RolloutGroupStatus.SCHEDULED)); } @@ -470,8 +473,9 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { assertThat(rollout.getStatus()).isEqualTo(RolloutStatus.PAUSED); // all other groups should still be in scheduled state - final List scheduleGroups = rolloutGroupManagement.findByRollout( - new OffsetBasedPageRequest(1, 100, Sort.by(Direction.ASC, "id")), createdRollout.getId()).getContent(); + final List scheduleGroups = rolloutGroupManagement + .findByRollout(new OffsetBasedPageRequest(1, 100, Sort.by(Direction.ASC, "id")), createdRollout.getId()) + .getContent(); scheduleGroups.forEach(group -> assertThat(group.getStatus()).isEqualTo(RolloutGroupStatus.SCHEDULED)); // resume the rollout again after it gets paused by error action @@ -521,8 +525,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { // verify all groups are in finished state rolloutGroupManagement - .findByRollout(new OffsetBasedPageRequest(0, 100, Sort.by(Direction.ASC, "id")), - createdRollout.getId()) + .findByRollout(new OffsetBasedPageRequest(0, 100, Sort.by(Direction.ASC, "id")), createdRollout.getId()) .forEach(group -> assertThat(group.getStatus()).isEqualTo(RolloutGroupStatus.FINISHED)); // verify that rollout itself is in finished state @@ -1823,6 +1826,56 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { assertThat(actions).allMatch(action -> !action.getWeight().isPresent()); } + @Test + @Description("Verifies that an exception is thrown when trying to create a rollout with an invalidated distribution set.") + public void createRolloutWithInvalidDistributionSet() { + final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet(); + + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception") + .isThrownBy(() -> testdataFactory.createRolloutByVariables("createRolloutWithInvalidDistributionSet", + "desc", 2, "name==*", distributionSet, "50", "80")); + } + + @Test + @Description("Verifies that an exception is thrown when trying to create a rollout with an incomplete distribution set.") + public void createRolloutWithIncompleteDistributionSet() { + final DistributionSet distributionSet = testdataFactory.createIncompleteDistributionSet(); + + assertThatExceptionOfType(IncompleteDistributionSetException.class) + .as("Incomplete distributionSet should throw an exception") + .isThrownBy(() -> testdataFactory.createRolloutByVariables("createRolloutWithIncompleteDistributionSet", + "desc", 2, "name==*", distributionSet, "50", "80")); + } + + @Test + @Description("Verifies that an exception is thrown when trying to update a rollout with an invalidated distribution set.") + public void updateRolloutWithInvalidDistributionSet() { + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); + testdataFactory.createTarget(); + final Rollout rollout = testdataFactory.createRolloutByVariables("updateRolloutWithInvalidDistributionSet", + "desc", 2, "name==*", distributionSet, "50", "80"); + final DistributionSet invalidDistributionSet = testdataFactory.createAndInvalidateDistributionSet(); + + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception").isThrownBy(() -> rolloutManagement + .update(entityFactory.rollout().update(rollout.getId()).set(invalidDistributionSet.getId()))); + } + + @Test + @Description("Verifies that an exception is thrown when trying to update a rollout with an incomplete distribution set.") + public void updateRolloutWithIncompleteDistributionSet() { + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); + testdataFactory.createTarget(); + final Rollout rollout = testdataFactory.createRolloutByVariables("updateRolloutWithIncompleteDistributionSet", + "desc", 2, "name==*", distributionSet, "50", "80"); + final DistributionSet incompleteDistributionSet = testdataFactory.createIncompleteDistributionSet(); + + assertThatExceptionOfType(IncompleteDistributionSetException.class) + .as("Incomplete distributionSet should throw an exception").isThrownBy(() -> rolloutManagement.update( + entityFactory.rollout().update(rollout.getId()).set(incompleteDistributionSet.getId()))); + } + private RolloutGroupCreate generateRolloutGroup(final int index, final Integer percentage, final String targetFilter) { return entityFactory.rolloutGroup().create().name("Group" + index).description("Group" + index + "desc") diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java index 075da504b..07aef02f3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java @@ -23,14 +23,17 @@ import javax.validation.ConstraintViolationException; import org.assertj.core.api.Assertions; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; +import org.eclipse.hawkbit.repository.builder.AutoAssignDistributionSetUpdate; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetFilterQueryCreatedEvent; import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException; -import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignDistributionSetException; +import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.model.Action; @@ -51,7 +54,7 @@ import io.qameta.allure.Story; /** * Test class for {@link TargetFilterQueryManagement}. - * + * */ @Feature("Component Tests - Repository") @Story("Target Filter Query Management") @@ -252,7 +255,7 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest .create(entityFactory.distributionSet().create().name("incomplete").version("1") .type(testdataFactory.findOrCreateDefaultTestDsType())); - assertThatExceptionOfType(InvalidAutoAssignDistributionSetException.class) + assertThatExceptionOfType(IncompleteDistributionSetException.class) .isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() .updateAutoAssign(targetFilterQuery.getId()).ds(incompleteDistributionSet.getId()))); } @@ -263,7 +266,7 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest assignDistributionSet(softDeletedDs, testdataFactory.createTarget("forSoftDeletedDs")); distributionSetManagement.delete(softDeletedDs.getId()); - assertThatExceptionOfType(InvalidAutoAssignDistributionSetException.class) + assertThatExceptionOfType(EntityNotFoundException.class) .isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() .updateAutoAssign(targetFilterQuery.getId()).ds(softDeletedDs.getId()))); } @@ -502,4 +505,60 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest assertThat(targetFilterQueryManagement.get(filterId).get().getAutoAssignWeight().get()) .isEqualTo(Action.WEIGHT_MIN); } + + @Test + @Description("Verifies that an exception is thrown when trying to create a target filter with an invalidated distribution set.") + public void createTargetFilterWithInvalidDistributionSet() { + final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet(); + + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception") + .isThrownBy(() -> targetFilterQueryManagement.create( + entityFactory.targetFilterQuery().create().name("createTargetFilterWithInvalidDistributionSet") + .query("name==*").autoAssignDistributionSet(distributionSet))); + } + + @Test + @Description("Verifies that an exception is thrown when trying to create a target filter with an incomplete distribution set.") + public void createTargetFilterWithIncompleteDistributionSet() { + final DistributionSet distributionSet = testdataFactory.createIncompleteDistributionSet(); + + assertThatExceptionOfType(IncompleteDistributionSetException.class) + .as("Incomplete distributionSet should throw an exception") + .isThrownBy(() -> targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create() + .name("createTargetFilterWithIncompleteDistributionSet").query("name==*") + .autoAssignDistributionSet(distributionSet))); + } + + @Test + @Description("Verifies that an exception is thrown when trying to update a target filter with an invalidated distribution set.") + public void updateAutoAssignDsWithInvalidDistributionSet() { + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement + .create(entityFactory.targetFilterQuery().create().name("updateAutoAssignDsWithInvalidDistributionSet") + .query("name==*").autoAssignDistributionSet(distributionSet)); + final DistributionSet invalidDistributionSet = testdataFactory.createAndInvalidateDistributionSet(); + + assertThatExceptionOfType(InvalidDistributionSetException.class) + .as("Invalid distributionSet should throw an exception") + .isThrownBy(() -> targetFilterQueryManagement + .updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()) + .ds(invalidDistributionSet.getId()))); + } + + @Test + @Description("Verifies that an exception is thrown when trying to update a target filter with an incomplete distribution set.") + public void updateAutoAssignDsWithIncompleteDistributionSet() { + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create( + entityFactory.targetFilterQuery().create().name("updateAutoAssignDsWithIncompleteDistributionSet") + .query("name==*").autoAssignDistributionSet(distributionSet)); + final DistributionSet incompleteDistributionSet = testdataFactory.createIncompleteDistributionSet(); + + assertThatExceptionOfType(IncompleteDistributionSetException.class) + .as("Incomplete distributionSet should throw an exception") + .isThrownBy(() -> targetFilterQueryManagement + .updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()) + .ds(incompleteDistributionSet.getId()))); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignCheckerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignCheckerTest.java index bd103ff60..87b97040c 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignCheckerTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignCheckerTest.java @@ -15,7 +15,7 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignDistributionSetException; +import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; import org.eclipse.hawkbit.repository.jpa.ActionRepository; import org.eclipse.hawkbit.repository.model.Action; @@ -105,11 +105,11 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest { final DistributionSet setB = testdataFactory.createDistributionSet("dsB"); // target filter query that matches all targets - final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.updateAutoAssignDS( - entityFactory.targetFilterQuery() - .updateAutoAssign(targetFilterQueryManagement.create( - entityFactory.targetFilterQuery().create().name("filterA").query("name==*")).getId()) - .ds(setA.getId())); + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.updateAutoAssignDS(entityFactory + .targetFilterQuery() + .updateAutoAssign(targetFilterQueryManagement + .create(entityFactory.targetFilterQuery().create().name("filterA").query("name==*")).getId()) + .ds(setA.getId())); final String targetDsAIdPref = "targ"; final List targets = testdataFactory.createTargets(100, targetDsAIdPref, @@ -159,7 +159,7 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest { // target filter query that matches first bunch of targets, that should // fail - assertThatExceptionOfType(InvalidAutoAssignDistributionSetException.class).isThrownBy(() -> { + assertThatExceptionOfType(IncompleteDistributionSetException.class).isThrownBy(() -> { final Long filterId = targetFilterQueryManagement.create( entityFactory.targetFilterQuery().create().name("filterA").query("id==" + targetDsFIdPref + "*")) .getId(); @@ -221,10 +221,10 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest { final DistributionSet distributionSet, final List targets) { final Set targetIds = targets.stream().map(Target::getControllerId).collect(Collectors.toSet()); - actionRepository.findByDistributionSetId(Pageable.unpaged(), distributionSet.getId()) - .stream().filter(a -> targetIds.contains(a.getTarget().getControllerId())) - .forEach(a -> assertThat(a.getInitiatedBy()).as( - "Action should be initiated by the user who initiated the auto assignment") + actionRepository.findByDistributionSetId(Pageable.unpaged(), distributionSet.getId()).stream() + .filter(a -> targetIds.contains(a.getTarget().getControllerId())) + .forEach(a -> assertThat(a.getInitiatedBy()) + .as("Action should be initiated by the user who initiated the auto assignment") .isEqualTo(targetFilterQuery.getAutoAssignInitiatedBy())); } diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java index 0ea641459..9ac187b89 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java @@ -31,6 +31,7 @@ import org.eclipse.hawkbit.cache.TenantAwareCacheManager; import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.ControllerManagement; import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetTagManagement; import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; @@ -159,6 +160,9 @@ public abstract class AbstractIntegrationTest { @Autowired protected DeploymentManagement deploymentManagement; + @Autowired + protected DistributionSetInvalidationManagement distributionSetInvalidationManagement; + @Autowired protected ArtifactManagement artifactManagement; diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java index f4d4dfa6b..f093edbbb 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java @@ -28,6 +28,7 @@ import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.Constants; import org.eclipse.hawkbit.repository.ControllerManagement; import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetTagManagement; import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; @@ -48,6 +49,8 @@ import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.ArtifactUpload; import org.eclipse.hawkbit.repository.model.BaseEntity; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.DistributionSetTag; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.NamedEntity; @@ -140,6 +143,9 @@ public class TestdataFactory { @Autowired private DistributionSetManagement distributionSetManagement; + @Autowired + private DistributionSetInvalidationManagement distributionSetInvalidationManagement; + @Autowired private DistributionSetTypeManagement distributionSetTypeManagement; @@ -172,11 +178,11 @@ public class TestdataFactory { * {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} * , {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} and * {@link DistributionSet#isRequiredMigrationStep()} false. - * + * * @param prefix * for {@link SoftwareModule}s and {@link DistributionSet}s name, * vendor and description. - * + * * @return {@link DistributionSet} entity. */ public DistributionSet createDistributionSet(final String prefix) { @@ -188,7 +194,7 @@ public class TestdataFactory { * {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} * , {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} and * {@link DistributionSet#isRequiredMigrationStep()} false. - * + * * @return {@link DistributionSet} entity. */ public DistributionSet createDistributionSet() { @@ -200,10 +206,10 @@ public class TestdataFactory { * {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} * , {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} and * {@link DistributionSet#isRequiredMigrationStep()} false. - * + * * @param modules * of {@link DistributionSet#getModules()} - * + * * @return {@link DistributionSet} entity. */ public DistributionSet createDistributionSet(final Collection modules) { @@ -215,13 +221,13 @@ public class TestdataFactory { * {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} * , {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} and * {@link DistributionSet#isRequiredMigrationStep()} false. - * + * * @param modules * of {@link DistributionSet#getModules()} * @param prefix * for {@link SoftwareModule}s and {@link DistributionSet}s name, * vendor and description. - * + * * @return {@link DistributionSet} entity. */ public DistributionSet createDistributionSet(final Collection modules, final String prefix) { @@ -232,13 +238,13 @@ public class TestdataFactory { * Creates {@link DistributionSet} in repository including three * {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} * , {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION}. - * + * * @param prefix * for {@link SoftwareModule}s and {@link DistributionSet}s name, * vendor and description. * @param isRequiredMigrationStep * for {@link DistributionSet#isRequiredMigrationStep()} - * + * * @return {@link DistributionSet} entity. */ public DistributionSet createDistributionSet(final String prefix, final boolean isRequiredMigrationStep) { @@ -250,13 +256,13 @@ public class TestdataFactory { * {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} * , {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} and * {@link DistributionSet#isRequiredMigrationStep()} false. - * + * * @param prefix * for {@link SoftwareModule}s and {@link DistributionSet}s name, * vendor and description. * @param tags * {@link DistributionSet#getTags()} - * + * * @return {@link DistributionSet} entity. */ public DistributionSet createDistributionSet(final String prefix, final Collection tags) { @@ -267,7 +273,7 @@ public class TestdataFactory { * Creates {@link DistributionSet} in repository including three * {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} * , {@link #SM_TYPE_APP}. - * + * * @param prefix * for {@link SoftwareModule}s and {@link DistributionSet}s name, * vendor and description. @@ -277,7 +283,7 @@ public class TestdataFactory { * number. * @param isRequiredMigrationStep * for {@link DistributionSet#isRequiredMigrationStep()} - * + * * @return {@link DistributionSet} entity. */ public DistributionSet createDistributionSet(final String prefix, final String version, @@ -306,12 +312,12 @@ public class TestdataFactory { /** * Adds {@link SoftwareModuleMetadata} to every module of given * {@link DistributionSet}. - * + * * {@link #VISIBLE_SM_MD_VALUE}, {@link #VISIBLE_SM_MD_KEY} with * {@link SoftwareModuleMetadata#isTargetVisible()} and * {@link #INVISIBLE_SM_MD_KEY}, {@link #INVISIBLE_SM_MD_VALUE} without * {@link SoftwareModuleMetadata#isTargetVisible()} - * + * * @param set * to add metadata to */ @@ -329,7 +335,7 @@ public class TestdataFactory { /** * Creates {@link DistributionSet} in repository. - * + * * @param prefix * for {@link SoftwareModule}s and {@link DistributionSet}s name, * vendor and description. @@ -341,7 +347,7 @@ public class TestdataFactory { * for {@link DistributionSet#isRequiredMigrationStep()} * @param modules * for {@link DistributionSet#getModules()} - * + * * @return {@link DistributionSet} entity. */ public DistributionSet createDistributionSet(final String prefix, final String version, @@ -358,7 +364,7 @@ public class TestdataFactory { * Creates {@link DistributionSet} in repository including three * {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} * , {@link #SM_TYPE_APP}. - * + * * @param prefix * for {@link SoftwareModule}s and {@link DistributionSet}s name, * vendor and description. @@ -368,7 +374,7 @@ public class TestdataFactory { * number.updat * @param tags * {@link DistributionSet#getTags()} - * + * * @return {@link DistributionSet} entity. */ public DistributionSet createDistributionSet(final String prefix, final String version, @@ -388,10 +394,10 @@ public class TestdataFactory { * , {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} followed by an * iterative number and {@link DistributionSet#isRequiredMigrationStep()} * false. - * + * * @param number * of {@link DistributionSet}s to create - * + * * @return {@link List} of {@link DistributionSet} entities */ public List createDistributionSets(final int number) { @@ -402,7 +408,7 @@ public class TestdataFactory { /** * Create a list of {@link DistributionSet}s without modules, i.e. * incomplete. - * + * * @param number * of {@link DistributionSet}s to create * @return {@link List} of {@link DistributionSet} entities @@ -425,13 +431,13 @@ public class TestdataFactory { * , {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} followed by an * iterative number and {@link DistributionSet#isRequiredMigrationStep()} * false. - * + * * @param prefix * for {@link SoftwareModule}s and {@link DistributionSet}s name, * vendor and description. * @param number * of {@link DistributionSet}s to create - * + * * @return {@link List} of {@link DistributionSet} entities */ public List createDistributionSets(final String prefix, final int number) { @@ -448,12 +454,12 @@ public class TestdataFactory { * Creates {@link DistributionSet}s in repository with * {@link #DEFAULT_DESCRIPTION} and * {@link DistributionSet#isRequiredMigrationStep()} false. - * + * * @param name * {@link DistributionSet#getName()} * @param version * {@link DistributionSet#getVersion()} - * + * * @return {@link DistributionSet} entity */ public DistributionSet createDistributionSetWithNoSoftwareModules(final String name, final String version) { @@ -465,10 +471,10 @@ public class TestdataFactory { /** * Creates {@link Artifact}s for given {@link SoftwareModule} with a small * text payload. - * + * * @param moduleId * the {@link Artifact}s belong to. - * + * * @return {@link Artifact} entity. */ public List createArtifacts(final Long moduleId) { @@ -484,16 +490,16 @@ public class TestdataFactory { /** * Create an {@link Artifact} for given {@link SoftwareModule} with a small * text payload. - * + * * @param artifactData * the {@link Artifact} Inputstream - * + * * @param moduleId * the {@link Artifact} belongs to - * + * * @param filename * that was provided during upload. - * + * * @return {@link Artifact} entity. */ public Artifact createArtifact(final String artifactData, final Long moduleId, final String filename) { @@ -530,10 +536,10 @@ public class TestdataFactory { * Creates {@link SoftwareModule} with {@link #DEFAULT_VENDOR} and * {@link #DEFAULT_VERSION} and random generated * {@link Target#getDescription()} in the repository. - * + * * @param typeKey * of the {@link SoftwareModuleType} - * + * * @return persisted {@link SoftwareModule}. */ public SoftwareModule createSoftwareModule(final String typeKey) { @@ -545,8 +551,8 @@ public class TestdataFactory { * {@value Constants#SMT_DEFAULT_APP_KEY} with {@link #DEFAULT_VENDOR} and * {@link #DEFAULT_VERSION} and random generated * {@link Target#getDescription()} in the repository. - * - * + * + * * @return persisted {@link SoftwareModule}. */ public SoftwareModule createSoftwareModuleApp() { @@ -558,11 +564,11 @@ public class TestdataFactory { * {@value Constants#SMT_DEFAULT_APP_KEY} with {@link #DEFAULT_VENDOR} and * {@link #DEFAULT_VERSION} and random generated * {@link Target#getDescription()} in the repository. - * + * * @param prefix * added to name and version - * - * + * + * * @return persisted {@link SoftwareModule}. */ public SoftwareModule createSoftwareModuleApp(final String prefix) { @@ -574,8 +580,8 @@ public class TestdataFactory { * {@value Constants#SMT_DEFAULT_OS_KEY} with {@link #DEFAULT_VENDOR} and * {@link #DEFAULT_VERSION} and random generated * {@link Target#getDescription()} in the repository. - * - * + * + * * @return persisted {@link SoftwareModule}. */ public SoftwareModule createSoftwareModuleOs() { @@ -587,11 +593,11 @@ public class TestdataFactory { * {@value Constants#SMT_DEFAULT_OS_KEY} with {@link #DEFAULT_VENDOR} and * {@link #DEFAULT_VERSION} and random generated * {@link Target#getDescription()} in the repository. - * + * * @param prefix * added to name and version - * - * + * + * * @return persisted {@link SoftwareModule}. */ public SoftwareModule createSoftwareModuleOs(final String prefix) { @@ -602,12 +608,12 @@ public class TestdataFactory { * Creates {@link SoftwareModule} with {@link #DEFAULT_VENDOR} and * {@link #DEFAULT_VERSION} and random generated * {@link Target#getDescription()} in the repository. - * + * * @param typeKey * of the {@link SoftwareModuleType} * @param prefix * added to name and version - * + * * @return persisted {@link SoftwareModule}. */ public SoftwareModule createSoftwareModule(final String typeKey, final String prefix) { @@ -652,12 +658,12 @@ public class TestdataFactory { * @param targetName * name of the target * @param targetTypeId - * target type id + * target type id * @return persisted {@link Target} */ public Target createTarget(final String controllerId, final String targetName, final Long targetTypeId) { - final Target target = targetManagement - .create(entityFactory.target().create().controllerId(controllerId).name(targetName).targetType(targetTypeId)); + final Target target = targetManagement.create( + entityFactory.target().create().controllerId(controllerId).name(targetName).targetType(targetTypeId)); assertTargetProperlyCreated(target); return target; } @@ -677,12 +683,12 @@ public class TestdataFactory { * , {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} followed by an * iterative number and {@link DistributionSet#isRequiredMigrationStep()} * false. - * + * * In addition it updates the created {@link DistributionSet}s and * {@link SoftwareModule}s to ensure that * {@link BaseEntity#getLastModifiedAt()} and * {@link BaseEntity#getLastModifiedBy()} is filled. - * + * * @return persisted {@link DistributionSet}. */ public DistributionSet createUpdatedDistributionSet() { @@ -716,12 +722,12 @@ public class TestdataFactory { /** * Creates {@link DistributionSetType} in repository. - * + * * @param dsTypeKey * {@link DistributionSetType#getKey()} * @param dsTypeName * {@link DistributionSetType#getName()} - * + * * @return persisted {@link DistributionSetType} */ public DistributionSetType findOrCreateDistributionSetType(final String dsTypeKey, final String dsTypeName) { @@ -733,7 +739,7 @@ public class TestdataFactory { /** * Finds {@link DistributionSetType} in repository with given * {@link DistributionSetType#getKey()} or creates if it does not exist yet. - * + * * @param dsTypeKey * {@link DistributionSetType#getKey()} * @param dsTypeName @@ -742,7 +748,7 @@ public class TestdataFactory { * {@link DistributionSetType#getMandatoryModuleTypes()} * @param optional * {@link DistributionSetType#getOptionalModuleTypes()} - * + * * @return persisted {@link DistributionSetType} */ public DistributionSetType findOrCreateDistributionSetType(final String dsTypeKey, final String dsTypeName, @@ -758,10 +764,10 @@ public class TestdataFactory { * Finds {@link SoftwareModuleType} in repository with given * {@link SoftwareModuleType#getKey()} or creates if it does not exist yet * with {@link SoftwareModuleType#getMaxAssignments()} = 1. - * + * * @param key * {@link SoftwareModuleType#getKey()} - * + * * @return persisted {@link SoftwareModuleType} */ public SoftwareModuleType findOrCreateSoftwareModuleType(final String key) { @@ -771,18 +777,18 @@ public class TestdataFactory { /** * Finds {@link SoftwareModuleType} in repository with given * {@link SoftwareModuleType#getKey()} or creates if it does not exist yet. - * + * * @param key * {@link SoftwareModuleType#getKey()} * @param maxAssignments * {@link SoftwareModuleType#getMaxAssignments()} - * + * * @return persisted {@link SoftwareModuleType} */ public SoftwareModuleType findOrCreateSoftwareModuleType(final String key, final int maxAssignments) { - return softwareModuleTypeManagement.getByKey(key) - .orElseGet(() -> softwareModuleTypeManagement.create(entityFactory.softwareModuleType().create() - .key(key).name(key).description(LOREM.words(10)).colour("#ffffff").maxAssignments(maxAssignments))); + return softwareModuleTypeManagement.getByKey(key).orElseGet( + () -> softwareModuleTypeManagement.create(entityFactory.softwareModuleType().create().key(key).name(key) + .description(LOREM.words(10)).colour("#ffffff").maxAssignments(maxAssignments))); } /** @@ -796,7 +802,7 @@ public class TestdataFactory { * {@link DistributionSet#getType()} * @param modules * {@link DistributionSet#getModules()} - * + * * @return the created {@link DistributionSet} */ public DistributionSet createDistributionSet(final String name, final String version, @@ -819,7 +825,7 @@ public class TestdataFactory { * {@link DistributionSet#getModules()} * @param requiredMigrationStep * {@link DistributionSet#isRequiredMigrationStep()} - * + * * @return the created {@link DistributionSet} */ public DistributionSet generateDistributionSet(final String name, final String version, @@ -841,7 +847,7 @@ public class TestdataFactory { * {@link DistributionSet#getType()} * @param modules * {@link DistributionSet#getModules()} - * + * * @return the created {@link DistributionSet} */ public DistributionSet generateDistributionSet(final String name, final String version, @@ -854,7 +860,7 @@ public class TestdataFactory { * * @param name * {@link DistributionSet#getName()} - * + * * @return the generated {@link DistributionSet} */ public DistributionSet generateDistributionSet(final String name) { @@ -866,10 +872,10 @@ public class TestdataFactory { * Creates {@link Target}s in repository and with * {@link #DEFAULT_CONTROLLER_ID} as prefix for * {@link Target#getControllerId()}. - * + * * @param number * of {@link Target}s to create - * + * * @return {@link List} of {@link Target} entities */ public List createTargets(final int number) { @@ -884,10 +890,10 @@ public class TestdataFactory { /** * Creates {@link Target}s in repository and with given targetIds. - * + * * @param targetIds * specifies the IDs of the targets - * + * * @return {@link List} of {@link Target} entities */ public List createTargets(final String... targetIds) { @@ -903,7 +909,7 @@ public class TestdataFactory { /** * Builds {@link Target} objects with given prefix for * {@link Target#getControllerId()} followed by a number suffix. - * + * * @param start * value for the controllerId suffix * @param numberOfTargets @@ -925,7 +931,7 @@ public class TestdataFactory { * Builds {@link Target} objects with given prefix for * {@link Target#getControllerId()} followed by a number suffix starting * with 0. - * + * * @param numberOfTargets * of {@link Target}s to generate * @param controllerIdPrefix @@ -1015,10 +1021,10 @@ public class TestdataFactory { /** * Creates {@link DistributionSetTag}s in repository. - * + * * @param number * of {@link DistributionSetTag}s - * + * * @return the persisted {@link DistributionSetTag}s */ public List createDistributionSetTags(final int number) { @@ -1042,14 +1048,14 @@ public class TestdataFactory { /** * Append {@link ActionStatus} to all {@link Action}s of given * {@link Target}s. - * + * * @param targets * to add {@link ActionStatus} * @param status * to add * @param message * to add - * + * * @return updated {@link Action}. */ public List sendUpdateActionStatusToTargets(final Collection targets, final Status status, @@ -1060,14 +1066,14 @@ public class TestdataFactory { /** * Append {@link ActionStatus} to all {@link Action}s of given * {@link Target}s. - * + * * @param targets * to add {@link ActionStatus} * @param status * to add * @param msgs * to add - * + * * @return updated {@link Action}. */ public List sendUpdateActionStatusToTargets(final Collection targets, final Status status, @@ -1085,7 +1091,7 @@ public class TestdataFactory { /** * Creates rollout based on given parameters. - * + * * @param rolloutName * of the {@link Rollout} * @param rolloutDescription @@ -1155,7 +1161,7 @@ public class TestdataFactory { /** * Create {@link Rollout} with a new {@link DistributionSet} and * {@link Target}s. - * + * * @param prefix * for rollouts name, description, * {@link Target#getControllerId()} filter @@ -1170,7 +1176,7 @@ public class TestdataFactory { /** * Create the soft deleted {@link Rollout} with a new * {@link DistributionSet} and {@link Target}s. - * + * * @param prefix * for rollouts name, description, * {@link Target#getControllerId()} filter @@ -1187,8 +1193,8 @@ public class TestdataFactory { /** * Finds {@link TargetType} in repository with given - * {@link TargetType#getName()} or creates if it does not exist yet. - * No ds types are assigned on creation. + * {@link TargetType#getName()} or creates if it does not exist yet. No ds + * types are assigned on creation. * * @param targetTypeName * {@link TargetType#getName()} @@ -1197,25 +1203,26 @@ public class TestdataFactory { */ public TargetType findOrCreateTargetType(final String targetTypeName) { return targetTypeManagement.getByName(targetTypeName) - .orElseGet(() -> targetTypeManagement.create(entityFactory.targetType().create() - .name(targetTypeName).description(targetTypeName + " description").colour(DEFAULT_COLOUR))); + .orElseGet(() -> targetTypeManagement.create(entityFactory.targetType().create().name(targetTypeName) + .description(targetTypeName + " description").colour(DEFAULT_COLOUR))); } - + /** * Creates {@link TargetType} in repository with given - * {@link TargetType#getName()}. Compatible distribution set types are assigned on creation + * {@link TargetType#getName()}. Compatible distribution set types are + * assigned on creation * * @param targetTypeName * {@link TargetType#getName()} * * @return persisted {@link TargetType} */ - public TargetType createTargetType(final String targetTypeName, List compatibleDsTypes) { - return targetTypeManagement.create(entityFactory.targetType().create() - .name(targetTypeName).description(targetTypeName + " description").colour(DEFAULT_COLOUR) - .compatible(compatibleDsTypes.stream().map(DistributionSetType::getId).collect(Collectors.toList()))); + public TargetType createTargetType(final String targetTypeName, final List compatibleDsTypes) { + return targetTypeManagement.create(entityFactory.targetType().create().name(targetTypeName) + .description(targetTypeName + " description").colour(DEFAULT_COLOUR) + .compatible(compatibleDsTypes.stream().map(DistributionSetType::getId).collect(Collectors.toList()))); } - + /** * Creates {@link TargetType} in repository with given * {@link TargetType#getName()}. No ds types are assigned on creation. @@ -1225,13 +1232,37 @@ public class TestdataFactory { * * @return persisted {@link TargetType} */ - public List createTargetTypes(final String targetTypePrefix, int count) { + public List createTargetTypes(final String targetTypePrefix, final int count) { final List result = Lists.newArrayListWithExpectedSize(count); for (int i = 0; i < count; i++) { - result.add(entityFactory.targetType().create().name(targetTypePrefix + i).description(targetTypePrefix + " description") - .colour(DEFAULT_COLOUR)); + result.add(entityFactory.targetType().create().name(targetTypePrefix + i) + .description(targetTypePrefix + " description").colour(DEFAULT_COLOUR)); } return targetTypeManagement.create(result); } + /** + * Creates a distribution set and directly invalidates it. No actions will + * be canceled and no rollouts will be stopped with this invalidation. + * + * @return created invalidated {@link DistributionSet} + */ + public DistributionSet createAndInvalidateDistributionSet() { + final DistributionSet distributionSet = createDistributionSet(); + distributionSetInvalidationManagement.invalidateDistributionSet( + new DistributionSetInvalidation(Arrays.asList(distributionSet.getId()), CancelationType.NONE, false)); + return distributionSet; + } + + /** + * Creates a distribution set that has no software modules assigned, so it + * is incomplete. + * + * @return created incomplete {@link DistributionSet} + */ + public DistributionSet createIncompleteDistributionSet() { + return distributionSetManagement.create(entityFactory.distributionSet().create() + .name(UUID.randomUUID().toString()).version(DEFAULT_VERSION).description(LOREM.words(10)) + .type(findOrCreateDefaultTestDsType()).requiredMigrationStep(false)); + } } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtCancelationType.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtCancelationType.java new file mode 100644 index 000000000..a7f6db4b1 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtCancelationType.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.mgmt.json.model.distributionset; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Definition of the action cancel type for the distribution set invalidation + * via REST management API. + * + */ +public enum MgmtCancelationType { + /** + * Actions will be soft canceled. + */ + SOFT("soft"), + + /** + * Actions will be force quit. + */ + FORCE("force"), + + /** + * No actions will be canceled. + */ + NONE("none"); + + private final String name; + + private MgmtCancelationType(final String name) { + this.name = name; + } + + @JsonValue + public String getName() { + return name; + } +} diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtDistributionSet.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtDistributionSet.java index 8d6be03ab..8519cfbec 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtDistributionSet.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtDistributionSet.java @@ -49,6 +49,17 @@ public class MgmtDistributionSet extends MgmtNamedEntity { @JsonProperty private boolean deleted; + @JsonProperty + private boolean valid; + + public boolean isValid() { + return valid; + } + + public void setValid(final boolean valid) { + this.valid = valid; + } + public boolean isDeleted() { return deleted; } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtInvalidateDistributionSetRequestBody.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtInvalidateDistributionSetRequestBody.java new file mode 100644 index 000000000..41a57ab2a --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtInvalidateDistributionSetRequestBody.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.mgmt.json.model.distributionset; + +import javax.validation.constraints.NotNull; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A json annotated rest model for invalidate DistributionSet requests. + * + */ +public class MgmtInvalidateDistributionSetRequestBody { + + @NotNull + @JsonProperty + private MgmtCancelationType actionCancelationType; + @JsonProperty + private boolean cancelRollouts; + + public MgmtCancelationType getActionCancelationType() { + return actionCancelationType; + } + + public void setActionCancelationType(final MgmtCancelationType actionCancelationType) { + this.actionCancelationType = actionCancelationType; + } + + public boolean isCancelRollouts() { + return cancelRollouts; + } + + public void setCancelRollouts(final boolean cancelRollouts) { + this.cancelRollouts = cancelRollouts; + } + +} diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetRestApi.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetRestApi.java index 44178605e..89fd7d1e7 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetRestApi.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetRestApi.java @@ -10,12 +10,15 @@ package org.eclipse.hawkbit.mgmt.rest.api; import java.util.List; +import javax.validation.Valid; + import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata; import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut; import org.eclipse.hawkbit.mgmt.json.model.PagedList; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPut; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtInvalidateDistributionSetRequestBody; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentRequestBody; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentResponseBody; import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule; @@ -378,4 +381,19 @@ public interface MgmtDistributionSetRestApi { @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) int pagingOffsetParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) int pagingLimitParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) String sortParam); + + /** + * Invalidates a distribution set + * + * @param distributionSetId + * the ID of the distribution set to invalidate + * @param invalidateRequestBody + * the definition if rollouts and actions should be canceled + * @return status OK if the invalidation was successful + */ + @PostMapping(value = "/{distributionSetId}/invalidate", consumes = { MediaTypes.HAL_JSON_VALUE, + MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE, + MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity invalidateDistributionSet(@PathVariable("distributionSetId") Long distributionSetId, + @Valid MgmtInvalidateDistributionSetRequestBody invalidateRequestBody); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetMapper.java index 2a7bb46fb..2a2d0665a 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetMapper.java @@ -110,6 +110,7 @@ public final class MgmtDistributionSetMapper { response.setComplete(distributionSet.isComplete()); response.setType(distributionSet.getType().getKey()); response.setDeleted(distributionSet.isDeleted()); + response.setValid(distributionSet.isValid()); distributionSet.getModules() .forEach(module -> response.getModules().add(MgmtSoftwareModuleMapper.toResponse(module))); diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java index 03d4184d7..f5f644774 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java @@ -9,17 +9,21 @@ package org.eclipse.hawkbit.mgmt.rest.resource; import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map.Entry; import java.util.stream.Collectors; +import javax.validation.Valid; + import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata; import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut; import org.eclipse.hawkbit.mgmt.json.model.PagedList; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPut; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtInvalidateDistributionSetRequestBody; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentRequestBody; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentResponseBody; import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule; @@ -29,6 +33,7 @@ import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery; import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; @@ -40,6 +45,7 @@ import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; @@ -81,11 +87,14 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { private final SystemSecurityContext systemSecurityContext; + private final DistributionSetInvalidationManagement distributionSetInvalidationManagement; + MgmtDistributionSetResource(final SoftwareModuleManagement softwareModuleManagement, final TargetManagement targetManagement, final TargetFilterQueryManagement targetFilterQueryManagement, final DeploymentManagement deployManagament, final SystemManagement systemManagement, final EntityFactory entityFactory, final DistributionSetManagement distributionSetManagement, - final SystemSecurityContext systemSecurityContext) { + final SystemSecurityContext systemSecurityContext, + final DistributionSetInvalidationManagement distributionSetInvalidationManagement) { this.softwareModuleManagement = softwareModuleManagement; this.targetManagement = targetManagement; this.targetFilterQueryManagement = targetFilterQueryManagement; @@ -94,6 +103,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { this.entityFactory = entityFactory; this.distributionSetManagement = distributionSetManagement; this.systemSecurityContext = systemSecurityContext; + this.distributionSetInvalidationManagement = distributionSetInvalidationManagement; } @Override @@ -125,7 +135,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { @Override public ResponseEntity getDistributionSet( @PathVariable("distributionSetId") final Long distributionSetId) { - final DistributionSet foundDs = findDistributionSetWithExceptionIfNotFound(distributionSetId); + final DistributionSet foundDs = distributionSetManagement.getOrElseThrowException(distributionSetId); final MgmtDistributionSet response = MgmtDistributionSetMapper.toResponse(foundDs); MgmtDistributionSetMapper.addLinks(foundDs, response); @@ -206,7 +216,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { // check if distribution set exists otherwise throw exception // immediately - findDistributionSetWithExceptionIfNotFound(distributionSetId); + distributionSetManagement.getOrElseThrowException(distributionSetId); final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); @@ -254,11 +264,10 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { final List> offlineAssignments = assignments.stream() .map(assignment -> new SimpleEntry(assignment.getId(), distributionSetId)) .collect(Collectors.toList()); - return ResponseEntity - .ok(MgmtDistributionSetMapper - .toResponse(deployManagament.offlineAssignedDistributionSets(offlineAssignments))); + return ResponseEntity.ok(MgmtDistributionSetMapper + .toResponse(deployManagament.offlineAssignedDistributionSets(offlineAssignments))); } - + final List deploymentRequests = assignments.stream() .map(assignment -> MgmtDeploymentRequestMapper.createAssignmentRequest(assignment, distributionSetId)) .collect(Collectors.toList()); @@ -374,8 +383,14 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { softwaremodules.getTotalElements())); } - private DistributionSet findDistributionSetWithExceptionIfNotFound(final Long distributionSetId) { - return distributionSetManagement.get(distributionSetId) - .orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, distributionSetId)); + @Override + public ResponseEntity invalidateDistributionSet( + @PathVariable("distributionSetId") final Long distributionSetId, + @Valid @RequestBody final MgmtInvalidateDistributionSetRequestBody invalidateRequestBody) { + distributionSetInvalidationManagement + .invalidateDistributionSet(new DistributionSetInvalidation(Arrays.asList(distributionSetId), + MgmtRestModelMapper.convertCancelationType(invalidateRequestBody.getActionCancelationType()), + invalidateRequestBody.isCancelRollouts())); + return ResponseEntity.ok().build(); } } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java index dc5a46411..d34786a12 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java @@ -11,7 +11,9 @@ package org.eclipse.hawkbit.mgmt.rest.resource; import org.eclipse.hawkbit.mgmt.json.model.MgmtBaseEntity; import org.eclipse.hawkbit.mgmt.json.model.MgmtNamedEntity; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtCancelationType; import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.NamedEntity; import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity; @@ -48,10 +50,10 @@ public final class MgmtRestModelMapper { /** * Convert the given {@link MgmtActionType} into a corresponding repository * {@link ActionType}. - * + * * @param actionTypeRest * the REST representation of the action type - * + * * @return or the repository action type */ public static ActionType convertActionType(final MgmtActionType actionTypeRest) { @@ -76,10 +78,10 @@ public final class MgmtRestModelMapper { /** * Converts the given repository {@link ActionType} into a corresponding * {@link MgmtActionType}. - * + * * @param actionType * the repository representation of the action type - * + * * @return or the REST action type */ public static MgmtActionType convertActionType(final ActionType actionType) { @@ -100,4 +102,30 @@ public final class MgmtRestModelMapper { throw new IllegalStateException("Action Type is not supported"); } } + + /** + * Converts the given repository {@link CancelationType} into a + * corresponding {@link MgmtCancelationType}. + * + * @param cancelationType + * the repository representation of the cancellation type + * + * @return or the REST cancellation type + */ + public static CancelationType convertCancelationType(final MgmtCancelationType cancelationType) { + if (cancelationType == null) { + return null; + } + + switch (cancelationType) { + case SOFT: + return CancelationType.SOFT; + case FORCE: + return CancelationType.FORCE; + case NONE: + return CancelationType.NONE; + default: + throw new IllegalStateException("Action Cancelation Type is not supported"); + } + } } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java index cd13f6051..b59976b0b 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java @@ -110,7 +110,8 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi { // exception is thrown targetFilterQueryManagement.verifyTargetFilterQuerySyntax(rolloutRequestBody.getTargetFilterQuery()); - final DistributionSet distributionSet = findDistributionSetOrThrowException(rolloutRequestBody); + final DistributionSet distributionSet = distributionSetManagement + .getValidAndComplete(rolloutRequestBody.getDistributionSetId()); final RolloutGroupConditions rolloutGroupConditions = MgmtRolloutMapper.fromRequest(rolloutRequestBody, true); final RolloutCreate create = MgmtRolloutMapper.fromRequest(entityFactory, rolloutRequestBody, distributionSet); @@ -234,10 +235,4 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi { final List rest = MgmtTargetMapper.toResponse(rolloutGroupTargets.getContent()); return ResponseEntity.ok(new PagedList<>(rest, rolloutGroupTargets.getTotalElements())); } - - private DistributionSet findDistributionSetOrThrowException(final MgmtRolloutRestRequestBody rolloutRequestBody) { - return this.distributionSetManagement.get(rolloutRequestBody.getDistributionSetId()).orElseThrow( - () -> new EntityNotFoundException(DistributionSet.class, rolloutRequestBody.getDistributionSetId())); - - } } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java index 5028a5205..fbf87caa9 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java @@ -40,8 +40,12 @@ import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; import org.eclipse.hawkbit.repository.model.NamedEntity; +import org.eclipse.hawkbit.repository.model.Rollout; +import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.test.util.TestdataFactory; import org.eclipse.hawkbit.repository.test.util.WithUser; import org.eclipse.hawkbit.rest.util.JsonBuilder; @@ -1350,4 +1354,37 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr assertThat(actions).size().isEqualTo(1); assertThat(actions.get(0).getWeight()).get().isEqualTo(weight); } + + @Test + @Description("Verify invalidation of distribution sets that removes distribution sets from auto assignments, stops rollouts and cancels assignments") + public void invalidateDistributionSet() throws Exception { + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); + final List targets = testdataFactory.createTargets(5, "invalidateDistributionSet"); + assignDistributionSet(distributionSet, targets); + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement + .create(entityFactory.targetFilterQuery().create().name("invalidateDistributionSet").query("name==*") + .autoAssignDistributionSet(distributionSet)); + final Rollout rollout = testdataFactory.createRolloutByVariables("invalidateDistributionSet", "desc", 2, + "name==*", distributionSet, "50", "80"); + + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("actionCancelationType", "soft"); + jsonObject.put("cancelRollouts", true); + + mvc.perform(post("/rest/v1/distributionsets/{ds}/invalidate", distributionSet.getId()) + .content(jsonObject.toString()).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + + assertThat(targetFilterQueryManagement.get(targetFilterQuery.getId()).get().getAutoAssignDistributionSet()) + .isNull(); + assertThat(rolloutManagement.get(rollout.getId()).get().getStatus()).isIn(RolloutStatus.STOPPING, + RolloutStatus.FINISHED); + 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.CANCELING); + } + } } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java index f97e7c0f1..645213182 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java @@ -25,9 +25,10 @@ import java.util.List; import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; -import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException; -import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignDistributionSetException; import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -294,7 +295,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte assertThat(exceptionInfo.getExceptionClass()).isEqualTo(MessageNotReadableException.class.getName()); assertThat(exceptionInfo.getErrorCode()).isEqualTo(SpServerError.SP_REST_BODY_NOT_READABLE.getKey()); } - + @Test @Description("Ensures that the creation of a target filter query based on an invalid RSQL query results in a HTTP Bad Request error (400).") public void createTargetFilterWithInvalidQuery() throws Exception { @@ -324,7 +325,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId() + "/autoAssignDS") .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) .andDo(print()).andExpect(status().isForbidden()) - .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(AssignmentQuotaExceededException.class.getName()))) + .andExpect( + jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(AssignmentQuotaExceededException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); } @@ -356,7 +358,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId()) .content("{\"query\":\"controllerId==target*\"}").contentType(MediaType.APPLICATION_JSON)) .andDo(print()).andExpect(status().isForbidden()) - .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(AssignmentQuotaExceededException.class.getName()))) + .andExpect( + jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(AssignmentQuotaExceededException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); } @@ -470,9 +473,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte .content("{\"id\":" + incompleteDistributionSet.getId() + "}").contentType(MediaType.APPLICATION_JSON)) .andDo(print()).andExpect(status().isBadRequest()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, - equalTo(InvalidAutoAssignDistributionSetException.class.getName()))) - .andExpect(jsonPath(JSON_PATH_ERROR_CODE, - equalTo(SpServerError.SP_AUTO_ASSIGN_DISTRIBUTION_SET_INVALID.getKey()))); + equalTo(IncompleteDistributionSetException.class.getName()))) + .andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_DS_INCOMPLETE.getKey()))); } @Step @@ -483,11 +485,9 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") .content("{\"id\":" + softDeletedDs.getId() + "}").contentType(MediaType.APPLICATION_JSON)) - .andDo(print()).andExpect(status().isBadRequest()) - .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, - equalTo(InvalidAutoAssignDistributionSetException.class.getName()))) - .andExpect(jsonPath(JSON_PATH_ERROR_CODE, - equalTo(SpServerError.SP_AUTO_ASSIGN_DISTRIBUTION_SET_INVALID.getKey()))); + .andDo(print()).andExpect(status().isNotFound()) + .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(EntityNotFoundException.class.getName()))) + .andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_REPO_ENTITY_NOT_EXISTS.getKey()))); } @Test diff --git a/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/exception/ResponseExceptionHandler.java b/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/exception/ResponseExceptionHandler.java index 2480a2118..3ebfac15e 100644 --- a/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/exception/ResponseExceptionHandler.java +++ b/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/exception/ResponseExceptionHandler.java @@ -83,11 +83,13 @@ public class ResponseExceptionHandler { ERROR_TO_HTTP_STATUS.put(SpServerError.SP_MAINTENANCE_SCHEDULE_INVALID, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_TARGET_ATTRIBUTES_INVALID, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_AUTO_ASSIGN_ACTION_TYPE_INVALID, HttpStatus.BAD_REQUEST); - ERROR_TO_HTTP_STATUS.put(SpServerError.SP_AUTO_ASSIGN_DISTRIBUTION_SET_INVALID, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED, HttpStatus.FORBIDDEN); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_MULTIASSIGNMENT_NOT_ENABLED, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_NO_WEIGHT_PROVIDED_IN_MULTIASSIGNMENT_MODE, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_TARGET_TYPE_IN_USE, HttpStatus.CONFLICT); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_INVALID, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_INCOMPLETE, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_STOP_ROLLOUT_FAILED, HttpStatus.LOCKED); } private static HttpStatus getStatusOrDefault(final SpServerError error) { diff --git a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/distributionsets-api-guide.adoc b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/distributionsets-api-guide.adoc index 40f0d00a5..c04e00011 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/distributionsets-api-guide.adoc +++ b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/distributionsets-api-guide.adoc @@ -830,6 +830,54 @@ include::../errors/429.adoc[] |=== +== POST /rest/v1/distributionsets/{distributionSetId}/invalidate + + +=== Implementation Notes + +Invalidate a distribution set. Once a distribution set is invalidated, it can not be valid again. An invalidated distribution set cannot be assigned to targets anymore. The distribution set that is going to be invalidated will be removed from all auto assignments. Furthermore, the user can choose to cancel all rollouts and (force) cancel all actions connected to this distribution set. Required permission: UPDATE_REPOSITORY + +=== Invalidate a distribution set + +==== Curl + +include::{snippets}/distributionsets/invalidate/curl-request.adoc[] + +==== Request URL + +include::{snippets}/distributionsets/invalidate/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/distributionsets/invalidate/path-parameters.adoc[] + +==== Request fields + +include::{snippets}/distributionsets/invalidate/request-fields.adoc[] + +=== Response (Status 200) + +==== Response example + +include::{snippets}/distributionsets/invalidate/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +include::../errors/404.adoc[] +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/409.adoc[] +include::../errors/415.adoc[] +include::../errors/429.adoc[] +|=== + + == Additional content [[error-body]] diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/AbstractApiRestDocumentation.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/AbstractApiRestDocumentation.java index 51d690623..6eec0d1be 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/AbstractApiRestDocumentation.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/AbstractApiRestDocumentation.java @@ -89,8 +89,9 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati protected String host = "management-api.host"; /** - * The generated REST docs snippets will be outputted to an own resource folder. - * The child class has to specify the name of that output folder where to put its corresponding snippets. + * The generated REST docs snippets will be outputted to an own resource + * folder. The child class has to specify the name of that output folder + * where to put its corresponding snippets. * * @return the name of the resource folder */ @@ -101,8 +102,8 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati this.document = document(getResourceName() + "/{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())); this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) - .apply(MockMvcRestDocumentation.documentationConfiguration(restDocContext).uris() - .withScheme("https").withHost(host + ".com").withPort(443)) + .apply(MockMvcRestDocumentation.documentationConfiguration(restDocContext).uris().withScheme("https") + .withHost(host + ".com").withPort(443)) .alwaysDo(this.document).addFilter(filterHttpResponse).build(); arrayPrefix = "[]"; } @@ -163,33 +164,36 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati protected Target createTargetByGivenNameWithAttributes(final String name, final boolean inSync, final boolean timeforced, final DistributionSet distributionSet) { - return createTargetByGivenNameWithAttributes(name, inSync, timeforced, distributionSet, null, null, null, false); + return createTargetByGivenNameWithAttributes(name, inSync, timeforced, distributionSet, null, null, null, + false); } protected Target createTargetByGivenNameWithAttributes(final String name, final boolean inSync, final boolean timeforced, final DistributionSet distributionSet, final boolean createRollout) { - return createTargetByGivenNameWithAttributes(name, inSync, timeforced, distributionSet, null, null, null, createRollout); + return createTargetByGivenNameWithAttributes(name, inSync, timeforced, distributionSet, null, null, null, + createRollout); } protected Target createTargetByGivenNameWithAttributes(final String name, final boolean inSync, final boolean timeforced, final DistributionSet distributionSet, final String maintenanceWindowSchedule, - final String maintenanceWindowDuration, final String maintenanceWindowTimeZone, final boolean createRollout) { + final String maintenanceWindowDuration, final String maintenanceWindowTimeZone, + final boolean createRollout) { final Target savedTarget = targetManagement.create(entityFactory.target().create().controllerId(name) .status(TargetUpdateStatus.UNKNOWN).address("http://192.168.0.1").description("My name is " + name) .lastTargetQuery(System.currentTimeMillis())); - + final List updatedTargets; if (createRollout) { final Rollout rollout = testdataFactory.createRolloutByVariables("rollout", "rollout desc", 1, "name==" + name, distributionSet, "50", "5", timeforced ? ActionType.TIMEFORCED : ActionType.FORCED, isMultiAssignmentsEnabled() ? 600 : null); - + // start the rollout and handle it rolloutManagement.start(rollout.getId()); rolloutManagement.handleRollouts(); - + updatedTargets = Collections.singletonList(savedTarget); } else { @@ -208,7 +212,7 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati updatedTargets = makeAssignment(deploymentRequestBuilder.build()).getAssignedEntity().stream() .map(Action::getTarget).collect(Collectors.toList()); } - + if (inSync) { feedbackToByInSync(distributionSet); } @@ -318,6 +322,7 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati .description(MgmtApiModelProperties.DS_REQUIRED_STEP), fieldWithPath(arrayPrefix + "complete").description(MgmtApiModelProperties.DS_COMPLETE), fieldWithPath(arrayPrefix + "deleted").description(ApiModelPropertiesGeneric.DELETED), + fieldWithPath(arrayPrefix + "valid").description(MgmtApiModelProperties.DS_VALID), fieldWithPath(arrayPrefix + "version").description(MgmtApiModelProperties.VERSION), fieldWithPath(arrayPrefix + "_links.self").ignored(), fieldWithPath(arrayPrefix + "modules").ignored()); diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java index 188cd4b84..4be80f99e 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java @@ -67,6 +67,8 @@ public final class MgmtApiModelProperties { public static final String DS_ALREADY_ASSIGNED_TARGETS = "Targets that had this distribution set already assigned (in \"offline\" case this includes targets that have arbitrary updates running)"; public static final String DS_TOTAL_ASSIGNED_TARGETS = "Overall assigned as part of this request."; public static final String DS_ID = "Id of the distribution set."; + public static final String DS_INVALIDATION_ACTION_CANCELATION_TYPE = "Type of cancelation for actions referring to the given distribution set."; + public static final String DS_INVALIDATION_CANCEL_ROLLOUTS = "Defines if rollouts referring to this distribution set should be canceled."; // Target public static final String INSTALLED_AT = "Installation time of current installed DistributionSet."; @@ -184,6 +186,8 @@ public final class MgmtApiModelProperties { public static final String DS_COMPLETE = "True of the distribution set software module setup is complete as defined by the distribution set type."; + public static final String DS_VALID = "True by default and false after the distribution set is invalidated by the user."; + public static final String DS_TYPE_MANDATORY_MODULES = "Mandatory module type IDs."; public static final String DS_TYPE_OPTIONAL_MODULES = "Optional module type IDs."; diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java index 05d7ef0af..b5a450bcb 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java @@ -379,8 +379,7 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat parameterWithName("distributionSetId").description(ApiModelPropertiesGeneric.ITEM_ID)), requestParameters(parameterWithName("offline") .description(MgmtApiModelProperties.OFFLINE_UPDATE).optional()), - requestFields( - requestFieldWithPath("[].id").description(ApiModelPropertiesGeneric.ITEM_ID), + requestFields(requestFieldWithPath("[].id").description(ApiModelPropertiesGeneric.ITEM_ID), requestFieldWithPathMandatoryInMultiAssignMode("[].weight") .description(MgmtApiModelProperties.ASSIGNMENT_WEIGHT) .type(JsonFieldType.NUMBER).attributes(key("value").value("0 - 1000")), @@ -395,8 +394,8 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat optionalRequestFieldWithPath("[].maintenanceWindow.timezone") .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE), optionalRequestFieldWithPath("[].type") - .description(MgmtApiModelProperties.ASSIGNMENT_TYPE) - .attributes(key("value").value("['soft', 'forced','timeforced', 'downloadonly']"))), + .description(MgmtApiModelProperties.ASSIGNMENT_TYPE).attributes( + key("value").value("['soft', 'forced','timeforced', 'downloadonly']"))), responseFields( fieldWithPath("assigned").description(MgmtApiModelProperties.DS_NEW_ASSIGNED_TARGETS), fieldWithPath("alreadyAssigned").type(JsonFieldType.NUMBER) @@ -445,8 +444,7 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat mockMvc.perform(delete( MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/{distributionSetId}/assignedSM/{softwareModuleId}", - set.getId(), set.findFirstModuleByType(osType).get().getId()) - .contentType(MediaType.APPLICATION_JSON)) + set.getId(), set.findFirstModuleByType(osType).get().getId()).contentType(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andDo(this.document.document(pathParameters( parameterWithName("distributionSetId").description(ApiModelPropertiesGeneric.ITEM_ID), @@ -667,4 +665,28 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat optionalRequestFieldWithPath("[]value") .description(MgmtApiModelProperties.META_DATA_VALUE)))); } + + @Test + @Description("Invalidates a distribution set. Required Permission: " + SpPermission.UPDATE_REPOSITORY) + public void invalidate() throws Exception { + final DistributionSet testDS = testdataFactory.createDistributionSet(); + + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("actionCancelationType", "soft"); + jsonObject.put("cancelRollouts", true); + + mockMvc.perform(post(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/{distributionSetId}/invalidate", + testDS.getId()).content(jsonObject.toString()).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andDo( + this.document.document( + pathParameters(parameterWithName("distributionSetId") + .description(ApiModelPropertiesGeneric.ITEM_ID)), + requestFields( + requestFieldWithPath("actionCancelationType") + .description( + MgmtApiModelProperties.DS_INVALIDATION_ACTION_CANCELATION_TYPE) + .attributes(key("value").value("['force','soft','none']")), + optionalRequestFieldWithPath("cancelRollouts") + .description(MgmtApiModelProperties.DS_INVALIDATION_CANCEL_ROLLOUTS)))); + } } diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java index 10f248c48..51a99ef8d 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java @@ -27,7 +27,7 @@ import org.springframework.security.core.GrantedAuthority; * *

* The Permissions cover CRUD for two data areas: - * + * * XX_Target_CRUD which covers the following entities: {@link Target} entities * including metadata, {@link TargetTag}s, {@link TargetRegistrationRule}s * XX_Repository CRUD which covers: {@link DistributionSet}s, @@ -157,7 +157,7 @@ public final class SpPermission { /** * Return all permission. - * + * * @param exclusionRoles * roles which will excluded * @return all permissions @@ -184,10 +184,10 @@ public final class SpPermission { * Contains all the spring security evaluation expressions for the * {@link PreAuthorize} annotation for method security. *

- * + * *

* Examples: - * + * * {@code * hasRole([role]) Returns true if the current principal has the specified role. * hasAnyRole([role1,role2]) Returns true if the current principal has any of the supplied roles (given as a comma-separated list of strings) @@ -331,6 +331,15 @@ public final class SpPermission { public static final String HAS_AUTH_UPDATE_REPOSITORY = HAS_AUTH_PREFIX + UPDATE_REPOSITORY + HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE; + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#READ_REPOSITORY} and + * {@link SpPermission#UPDATE_REPOSITORY} or {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_READ_REPOSITORY_AND_UPDATE_REPOSITORY = BRACKET_OPEN + HAS_AUTH_PREFIX + + READ_REPOSITORY + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + UPDATE_REPOSITORY + + HAS_AUTH_SUFFIX + BRACKET_CLOSE + HAS_AUTH_OR + IS_SYSTEM_CODE; + /** * Spring security eval hasAuthority expression to check if spring * context contains {@link SpPermission#READ_REPOSITORY} and