From 9cb5d313968d7f334a27c337ec6e98cabadf92ba Mon Sep 17 00:00:00 2001 From: Stefan Klotz <35995139+StefanKlt@users.noreply.github.com> Date: Fri, 8 Nov 2019 10:47:35 +0100 Subject: [PATCH] Prioritisation of assignments via mgmt-API (#895) * Updating the schema for targetfilterquery and rollout * Updating the weight validation logic and tests * Make weight optional * Fix existing multi assignment tests by adding weight, remove weight from TargetFilterQuery * Add weight validation tests, fix tests * Add mgmt api tests for assignment and getting action with weight * Add management layer validation and tests for creating rollouts with weight * Fix amqp test, add repo level validation to resource tests * Add weight to rollout mgmt-api and tests * Add weight to mgmt api target Filter create and update * Add target filter auto assign weight. disable enforcement of setting a weight in multiassign mode * Remove ignored tests, fix api doc * Fix minor findings * Fix findings * Remove hardcoded min weight * Add docu text, fix findings * Fix api documentation * Expose weight via DMF * Expose actions according to weight via ddi * Fix documentation * Add method to get actions ordered by weight to deploymentManagement * Updating the schema for targetfilterquery and rollout * Updated the indentation * Updated the helper class, fixed the randomUID in test factory * Updated the class name with prefix JPA * Adding the missing License for WeightValidationHelper class * Adding documentation to the dmf api on weight * Removed the merger markers * Updated the class name * Removed the redundant method * Addressed final PR comments * Updated the missing testcase with latest default weight value * Reverting the default value of weight back to 1000 and updated tests Signed-off-by: Shruthi Manavalli Ramanna Signed-off-by: Stefan Klotz --- docs/content/apis/dmf_api.md | 6 + docs/content/concepts/rollout-management.md | 7 +- .../hawkbit/exception/SpServerError.java | 8 +- .../hawkbit/repository/ActionFields.java | 6 +- .../amqp/AmqpMessageDispatcherService.java | 6 +- .../amqp/AmqpMessageHandlerService.java | 8 +- .../amqp/AmqpMessageHandlerServiceTest.java | 17 +- .../AbstractAmqpServiceIntegrationTest.java | 13 +- ...ssageDispatcherServiceIntegrationTest.java | 124 ++++++-- .../dmf/json/model/DmfMultiActionRequest.java | 13 +- .../repository/ControllerManagement.java | 180 +++++++----- .../repository/DeploymentManagement.java | 33 ++- .../repository/RepositoryProperties.java | 17 +- .../hawkbit/repository/RolloutManagement.java | 16 +- .../TargetFilterQueryManagement.java | 48 +-- .../AutoAssignDistributionSetUpdate.java | 94 ++++++ .../repository/builder/RolloutCreate.java | 25 ++ .../repository/builder/RolloutUpdate.java | 23 +- .../builder/TargetFilterQueryBuilder.java | 11 + .../builder/TargetFilterQueryCreate.java | 20 ++ .../builder/TargetFilterQueryUpdate.java | 1 - ...rovidedInMultiAssignmentModeException.java | 63 ++++ .../hawkbit/repository/model/Action.java | 21 +- .../repository/model/DeploymentRequest.java | 23 +- .../model/DeploymentRequestBuilder.java | 18 +- .../hawkbit/repository/model/Rollout.java | 7 +- .../repository/model/TargetFilterQuery.java | 7 + .../model/TargetWithActionType.java | 40 ++- .../repository/AbstractRolloutManagement.java | 12 +- .../builder/AbstractRolloutUpdateCreate.java | 60 ++++ ...AbstractTargetFilterQueryUpdateCreate.java | 54 ++++ .../builder/GenericRolloutUpdate.java | 1 - .../jpa/AbstractDsAssignmentStrategy.java | 1 + .../repository/jpa/ActionRepository.java | 34 ++- .../repository/jpa/JpaActionManagement.java | 57 ++++ .../jpa/JpaControllerManagement.java | 38 +-- .../jpa/JpaDeploymentManagement.java | 35 ++- .../repository/jpa/JpaRolloutManagement.java | 18 +- .../jpa/JpaTargetFilterQueryManagement.java | 40 +-- .../RepositoryApplicationConfiguration.java | 21 +- .../jpa/autoassign/AutoAssignChecker.java | 13 +- .../jpa/builder/JpaRolloutCreate.java | 1 + .../builder/JpaTargetFilterQueryBuilder.java | 6 + .../builder/JpaTargetFilterQueryCreate.java | 3 +- .../repository/jpa/model/JpaAction.java | 24 +- .../repository/jpa/model/JpaRollout.java | 20 +- .../jpa/model/JpaTargetFilterQuery.java | 24 +- .../jpa/utils/TenantConfigHelper.java | 53 ++++ .../jpa/utils/WeightValidationHelper.java | 132 +++++++++ .../DB2/V1_12_15__add_weight___DB2.sql | 3 + .../H2/V1_12_15__add_weight___H2.sql | 3 + .../MYSQL/V1_12_15__add_weight___MYSQL.sql | 3 + .../V1_12_15__add_weight___SQL_SERVER.sql | 3 + .../event/remote/RemoteIdEventTest.java | 1 - .../remote/RemoteTenantAwareEventTest.java | 1 - .../jpa/ControllerManagementTest.java | 106 ++++--- .../jpa/DeploymentManagementTest.java | 113 ++++++- .../repository/jpa/RolloutManagementTest.java | 103 ++++++- .../jpa/TargetFilterQueryManagementTest.java | 150 ++++++++-- .../jpa/TargetManagementSearchTest.java | 3 +- .../jpa/autoassign/AutoAssignCheckerTest.java | 69 +++-- .../jpa/rsql/RSQLActionFieldsTest.java | 2 + .../rsql/RSQLTargetFilterQueryFieldsTest.java | 12 +- .../test/util/AbstractIntegrationTest.java | 51 ++-- .../repository/test/util/TestdataFactory.java | 277 +++++++++++------- .../ddi/rest/resource/DdiRootController.java | 2 +- .../rest/resource/DdiRootControllerTest.java | 75 +++-- .../mgmt/json/model/action/MgmtAction.java | 14 +- .../MgmtTargetAssignmentRequestBody.java | 10 +- .../rollout/MgmtRolloutResponseBody.java | 13 +- .../rollout/MgmtRolloutRestRequestBody.java | 23 +- .../target/MgmtDistributionSetAssignment.java | 10 + .../MgmtDistributionSetAutoAssignment.java | 11 + .../targetfilter/MgmtTargetFilterQuery.java | 10 + .../MgmtTargetFilterQueryRequestBody.java | 3 +- .../resource/MgmtDeploymentRequestMapper.java | 10 +- .../resource/MgmtDistributionSetResource.java | 1 - .../mgmt/rest/resource/MgmtRolloutMapper.java | 5 +- .../resource/MgmtTargetFilterQueryMapper.java | 12 + .../MgmtTargetFilterQueryResource.java | 13 +- .../mgmt/rest/resource/MgmtTargetMapper.java | 2 +- .../AbstractManagementApiIntegrationTest.java | 23 ++ .../MgmtDistributionSetResourceTest.java | 63 ++-- .../resource/MgmtRolloutResourceTest.java | 42 ++- .../MgmtTargetFilterQueryResourceTest.java | 56 +++- .../rest/resource/MgmtTargetResourceTest.java | 103 +++++-- .../exception/ResponseExceptionHandler.java | 1 + .../hawkbit/rest/util/JsonBuilder.java | 16 +- .../AbstractApiRestDocumentation.java | 57 ++-- .../documentation/MgmtApiModelProperties.java | 5 + .../DistributionSetsDocumentationTest.java | 3 + .../RolloutResourceDocumentationTest.java | 253 ++++++++-------- ...ilterQueriesResourceDocumentationTest.java | 22 +- .../TargetResourceDocumentationTest.java | 20 +- .../DistributionSetSelectWindow.java | 11 +- .../FilterManagementView.java | 2 +- .../filtermanagement/TargetFilterTable.java | 5 +- .../TargetAssignmentOperations.java | 2 +- 98 files changed, 2425 insertions(+), 875 deletions(-) create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/AutoAssignDistributionSetUpdate.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/NoWeightProvidedInMultiAssignmentModeException.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaActionManagement.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/TenantConfigHelper.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/WeightValidationHelper.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_15__add_weight___DB2.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_15__add_weight___H2.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_15__add_weight___MYSQL.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_15__add_weight___SQL_SERVER.sql diff --git a/docs/content/apis/dmf_api.md b/docs/content/apis/dmf_api.md index 98ade42c4..372d25852 100644 --- a/docs/content/apis/dmf_api.md +++ b/docs/content/apis/dmf_api.md @@ -364,6 +364,8 @@ type=EVENT
tenant=tenant123
thingId=abc
topic=DOWNLOAD\_A If `multi.assignments.enabled` is enabled, this message is sent instead of DOWNLOAD_AND_INSTALL, DOWNLOAD, or CANCEL_DOWNLOAD by hawkBit to initialize update, download, or cancel task(s). + + With weight, one can set the priority to the action. The higher the weight, the higher is the priority of an action. Header | Description | Type | Mandatory ------- | -------------------------------- | ----------------------------------- | ------------------------------------------------------------- @@ -381,6 +383,7 @@ Payload Template (the Java representation is [DmfMultiActionRequest](https://git ```json [{ "topic": "String", +"weight": long, "action": { "actionId": long, "targetSecurityToken": "String", @@ -413,6 +416,7 @@ Payload Template (the Java representation is [DmfMultiActionRequest](https://git }, { "topic": "String", +"weight": long, "action": { "actionId": long, "targetSecurityToken": "String", @@ -454,6 +458,7 @@ type=EVENT
tenant=tenant123
thingId=abc
topic=MULTI\_ACTI ```json [{ "topic": "DOWNLOAD_AND_INSTALL", +"weight": 600, "action": { "actionId":137, "targetSecurityToken":"bH7XXAprK1ChnLfKSdtlsp7NOlPnZAYY", @@ -486,6 +491,7 @@ type=EVENT
tenant=tenant123
thingId=abc
topic=MULTI\_ACTI }, { "topic": "DOWNLOAD", +"weight": 500, "action": { "actionId":138, "targetSecurityToken":"bH7XXAprK1ChnLfKSdtlsp7NOlPnZAYY", diff --git a/docs/content/concepts/rollout-management.md b/docs/content/concepts/rollout-management.md index 21d6b416a..fe1cb2eed 100644 --- a/docs/content/concepts/rollout-management.md +++ b/docs/content/concepts/rollout-management.md @@ -52,6 +52,10 @@ The cascading execution of the deployment groups is based on two thresholds that One of the main paradigms of Eclipse hawkBit is, that a Distribution Set represents the currently installed software of a device. Hence, a device can have only one Distribution Set assigned/installed at a time. With _Multi-Assignments_ enabled, this paradigm shifts. Multi-Assignments allows to assign multiple Distribution Sets to a device simultaneously, without cancelling each other. As a consequence, an operator can trigger multiple campaigns addressing the same devices in parallel. +### Action weight + +To differentiate between important and less important updates a property called _weight_ is used. When multi-assignments is enabled every action has a weight value between (and including) 0 and 1000. The higher the weight the more important is the assignment represented by the action. Also when defining a _rollout_ or an _auto-assignment_ and multi-assignments is enabled a weight value has to be provided. This value is passed to the actions created during the execution of these _rollouts_ and _auto-assignments_. If no weight was provided the highest value of 1000 is used instead. + ### Consequences While this feature provides more flexibility to the user and enables new use-cases, there are also some consequences one should be aware of: @@ -60,11 +64,10 @@ While this feature provides more flexibility to the user and enables new use-cas * This feature is in beta and may change unannounced. * For now, this feature is **opt-in only**. Once activated, it cannot be deactivated. -* Currently, there is no mechanism to hint devices to process the actions in a certain order. **Minor** -* While on DMF-API a MULTI_ACTION request is sent, DDI-API only exposes the oldest open action. +* While on DMF-API a MULTI_ACTION request is sent, DDI-API only exposes the next action which has the highest priority in the list of open actions(according to their weight property). * All information regarding the currently assigned or installed Distribution Set does only respect the last assignment, as well as the last successfully installed Distribution set. This also affects: * Pinning a target or Distribution Set in Deployment View. * Statistics about installed or assigned Distribution Sets. 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 8d50b9e72..86757325c 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 @@ -256,7 +256,13 @@ public enum SpServerError { * */ SP_MULTIASSIGNMENT_NOT_ENABLED("hawkbit.server.error.multiassignmentNotEnabled", - "The requested operation requires Multiassignments to be enabled."); + "The requested operation requires multi assignments to be enabled."), + + /** + * + */ + 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."); private final String key; private final String message; diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java index 45b1a1993..e4a079bf3 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java @@ -21,7 +21,11 @@ public enum ActionFields implements FieldNameProvider, FieldValueConverter IpUtil.isAmqpUri(target.getAddress())).forEach(target -> { final List activeActions = deploymentManagement - .findActiveActionsByTarget(PageRequest.of(0, MAX_ACTION_COUNT), target.getControllerId()) - .getContent(); + .findActiveActionsWithHighestWeight(target.getControllerId(), MAX_ACTION_COUNT); activeActions.forEach(action -> action.getDistributionSet().getModules().forEach( module -> softwareModuleMetadata.computeIfAbsent(module, this::getSoftwareModuleMetadata))); @@ -204,7 +203,8 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { actions.forEach(action -> { final DmfActionRequest actionRequest = createDmfActionRequest(target, action, getSoftwareModuleMetaData.apply(action)); - multiActionRequest.addElement(getEventTypeForAction(action), actionRequest); + final int weight = deploymentManagement.getWeightConsideringDefault(action); + multiActionRequest.addElement(getEventTypeForAction(action), actionRequest, weight); }); final Message message = getMessageConverter().toMessage(multiActionRequest, diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java index 124d420b7..6eca6ca92 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java @@ -54,7 +54,6 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.MessageConversionException; -import org.springframework.data.domain.PageRequest; import org.springframework.messaging.handler.annotation.Header; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; @@ -233,8 +232,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService { LOG.debug("Target {} reported online state.", thingId); sendUpdateCommandToTarget(target); } catch (final EntityAlreadyExistsException e) { - throw new AmqpRejectAndDontRequeueException( - "Tried to register previously registered target, message will be ignored!", e); + throw new AmqpRejectAndDontRequeueException("Tried to register previously registered target, message will be ignored!", e); } } @@ -254,7 +252,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService { private void sendCurrentActionsAsMultiActionToTarget(final Target target) { final List actions = controllerManagement - .findActiveActionsByTarget(PageRequest.of(0, MAX_ACTION_COUNT), target.getControllerId()).getContent(); + .findActiveActionsWithHighestWeight(target.getControllerId(), MAX_ACTION_COUNT); final Set distributionSets = actions.stream().map(Action::getDistributionSet) .collect(Collectors.toSet()); @@ -267,7 +265,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService { private void sendOldestActionToTarget(final Target target) { final Optional actionOptional = controllerManagement - .findOldestActiveActionByTarget(target.getControllerId()); + .findActiveActionWithHighestWeight(target.getControllerId()); if (!actionOptional.isPresent()) { return; diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java index 4485a0fe8..0d5956266 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java @@ -181,17 +181,19 @@ public class AmqpMessageHandlerServiceTest { @Description("Tests the creation of a target/thing by calling the same method that incoming RabbitMQ messages would access.") public void createThing() { final String knownThingId = "1"; + final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED); + messageProperties.setHeader(MessageHeaderKey.THING_ID, "1"); + final Message message = messageConverter.toMessage(new byte[0], messageProperties); final Target targetMock = mock(Target.class); - targetIdCaptor = ArgumentCaptor.forClass(String.class); - uriCaptor = ArgumentCaptor.forClass(URI.class); + final ArgumentCaptor targetIdCaptor = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); when(controllerManagementMock.findOrRegisterTargetIfItDoesNotExist(targetIdCaptor.capture(), uriCaptor.capture())).thenReturn(targetMock); - when(controllerManagementMock.findOldestActiveActionByTarget(any())).thenReturn(Optional.empty()); + when(controllerManagementMock.findActiveActionWithHighestWeight(any())).thenReturn(Optional.empty()); - amqpMessageHandlerService.onMessage(createMessage(new byte[0], getThingCreatedMessageProperties(knownThingId)), - MessageType.THING_CREATED.name(), TENANT, VIRTUAL_HOST); + amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT, VIRTUAL_HOST); // verify assertThat(targetIdCaptor.getValue()).as("Thing id is wrong").isEqualTo(knownThingId); @@ -214,7 +216,7 @@ public class AmqpMessageHandlerServiceTest { when(controllerManagementMock.findOrRegisterTargetIfItDoesNotExist(targetIdCaptor.capture(), uriCaptor.capture(), targetNameCaptor.capture())).thenReturn(targetMock); - when(controllerManagementMock.findOldestActiveActionByTarget(any())).thenReturn(Optional.empty()); + when(controllerManagementMock.findActiveActionWithHighestWeight(any())).thenReturn(Optional.empty()); amqpMessageHandlerService.onMessage( createMessage(targetProperties, getThingCreatedMessageProperties(knownThingId)), @@ -488,7 +490,8 @@ public class AmqpMessageHandlerServiceTest { when(create.status(any())).thenReturn(create); when(entityFactoryMock.actionStatus()).thenReturn(builder); // for the test the same action can be used - when(controllerManagementMock.findOldestActiveActionByTarget(any())).thenReturn(Optional.of(action)); + when(controllerManagementMock.findActiveActionWithHighestWeight(any())) + .thenReturn(Optional.of(action)); final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT); messageProperties.setHeader(MessageHeaderKey.TOPIC, EventTopic.UPDATE_ACTION_STATUS.name()); diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java index 8ba84f587..0689ad038 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java @@ -188,16 +188,17 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt final DmfDownloadAndUpdateRequest downloadAndUpdateRequest = (DmfDownloadAndUpdateRequest) getDmfClient() .getMessageConverter().fromMessage(replyMessage); - Assert.assertThat(dsModules, - SoftwareModuleJsonMatcher.containsExactly(downloadAndUpdateRequest.getSoftwareModules())); + assertDmfDownloadAndUpdateRequest(downloadAndUpdateRequest, dsModules, controllerId); + } - downloadAndUpdateRequest.getSoftwareModules() + protected void assertDmfDownloadAndUpdateRequest(final DmfDownloadAndUpdateRequest request, + final Set softwareModules, final String controllerId) { + Assert.assertThat(softwareModules, SoftwareModuleJsonMatcher.containsExactly(request.getSoftwareModules())); + request.getSoftwareModules() .forEach(dmfModule -> assertThat(dmfModule.getMetadata()).containsExactly( new DmfMetadata(TestdataFactory.VISIBLE_SM_MD_KEY, TestdataFactory.VISIBLE_SM_MD_VALUE))); - final Target updatedTarget = waitUntilIsPresent(() -> targetManagement.getByControllerID(controllerId)); - - assertThat(updatedTarget.getSecurityToken()).isEqualTo(downloadAndUpdateRequest.getTargetSecurityToken()); + assertThat(updatedTarget.getSecurityToken()).isEqualTo(request.getTargetSecurityToken()); } protected void assertDownloadAndInstallMessage(final Set softwareModules, diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java index 50ea5a8d4..e807b9baa 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java @@ -15,6 +15,7 @@ import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ON import java.util.AbstractMap.SimpleEntry; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -26,11 +27,13 @@ import java.util.stream.Collectors; import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; +import org.eclipse.hawkbit.dmf.json.model.DmfActionRequest; import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus; import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest; import org.eclipse.hawkbit.dmf.json.model.DmfMultiActionRequest; import org.eclipse.hawkbit.dmf.json.model.DmfMultiActionRequest.DmfMultiActionElement; import org.eclipse.hawkbit.dmf.json.model.DmfSoftwareModule; +import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.event.remote.MultiActionEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; @@ -49,8 +52,10 @@ import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedE import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; +import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; +import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; @@ -100,7 +105,7 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer final String controllerId = TARGET_PREFIX + "sendDownloadStatusBeforeWindowStartTime"; registerAndAssertTargetWithExistingTenant(controllerId); - final DistributionSet distributionSet = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); testdataFactory.addSoftwareModuleMetadata(distributionSet); assignDistributionSetWithMaintenanceWindow(distributionSet.getId(), controllerId, getTestSchedule(2), getTestDuration(10), getTestTimeZone()); @@ -122,7 +127,7 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer final String controllerId = TARGET_PREFIX + "sendDAndIStatusMessageDuringWindow"; registerAndAssertTargetWithExistingTenant(controllerId); - final DistributionSet distributionSet = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); testdataFactory.addSoftwareModuleMetadata(distributionSet); assignDistributionSetWithMaintenanceWindow(distributionSet.getId(), controllerId, getTestSchedule(-5), getTestDuration(10), getTestTimeZone()); @@ -145,7 +150,7 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer final String controllerId = TARGET_PREFIX + "assignDistributionSetMultipleTimes"; final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId); - final DistributionSet distributionSet2 = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final DistributionSet distributionSet2 = testdataFactory.createDistributionSet(); testdataFactory.addSoftwareModuleMetadata(distributionSet2); assignDistributionSet(distributionSet2.getId(), controllerId); assertDownloadAndInstallMessage(distributionSet2.getModules(), controllerId); @@ -171,17 +176,74 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer final String controllerId = TARGET_PREFIX + "assignMultipleDsInMultiAssignMode"; registerAndAssertTargetWithExistingTenant(controllerId); - final Long actionId1 = assignNewDsToTarget(controllerId); + final Long actionId1 = assignNewDsToTarget(controllerId, 450); final Entry action1Install = new SimpleEntry<>(actionId1, EventTopic.DOWNLOAD_AND_INSTALL); waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); assertLatestMultiActionMessage(controllerId, Arrays.asList(action1Install)); - final Long actionId2 = assignNewDsToTarget(controllerId); + final Long actionId2 = assignNewDsToTarget(controllerId, 111); final Entry action2Install = new SimpleEntry<>(actionId2, EventTopic.DOWNLOAD_AND_INSTALL); waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); assertLatestMultiActionMessage(controllerId, Arrays.asList(action1Install, action2Install)); } + @Test + @Description("Verify payload of multi action messages.") + public void assertMultiActionMessagePayloads() { + final int expectedWeightIfNotSet = 1000; + final int weight1 = 600; + final String controllerId = UUID.randomUUID().toString(); + registerAndAssertTargetWithExistingTenant(controllerId); + final DistributionSet ds = testdataFactory.createDistributionSet(); + testdataFactory.addSoftwareModuleMetadata(ds); + + final Long installActionId = makeAssignment(DeploymentManagement.deploymentRequest(controllerId, ds.getId()) + .setActionType(ActionType.FORCED).build()).getAssignedEntity().get(0).getId(); + enableMultiAssignments(); + final Long downloadActionId = makeAssignment(DeploymentManagement.deploymentRequest(controllerId, ds.getId()) + .setActionType(ActionType.DOWNLOAD_ONLY).setWeight(weight1).build()).getAssignedEntity().get(0).getId(); + final Long cancelActionId = makeAssignment( + DeploymentManagement.deploymentRequest(controllerId, ds.getId()).setWeight(DEFAULT_TEST_WEIGHT).build()) + .getAssignedEntity().get(0).getId(); + // make sure the latest message in the queue is the one triggered by the + // cancellation + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.DOWNLOAD_AND_INSTALL, EventTopic.MULTI_ACTION, + EventTopic.MULTI_ACTION); + deploymentManagement.cancelAction(cancelActionId); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); + + final List multiActionMessages = getLatestMultiActionMessages(controllerId); + assertThat(multiActionMessages).hasSize(3); + final DmfMultiActionElement installMessage = multiActionMessages.stream() + .filter(message -> message.getTopic().equals(EventTopic.DOWNLOAD_AND_INSTALL)).findFirst().get(); + final DmfMultiActionElement downloadMessage = multiActionMessages.stream() + .filter(message -> message.getTopic().equals(EventTopic.DOWNLOAD)).findFirst().get(); + final DmfMultiActionElement cancelMessage = multiActionMessages.stream() + .filter(message -> message.getTopic().equals(EventTopic.CANCEL_DOWNLOAD)).findFirst().get(); + assertThat(installMessage.getWeight()).isEqualTo(expectedWeightIfNotSet); + assertThat(downloadMessage.getWeight()).isEqualTo(weight1); + assertThat(cancelMessage.getWeight()).isEqualTo(DEFAULT_TEST_WEIGHT); + + assertThat(installMessage.getAction()).isExactlyInstanceOf(DmfDownloadAndUpdateRequest.class) + .hasFieldOrPropertyWithValue("actionId", installActionId); + assertThat(downloadMessage.getAction()).isExactlyInstanceOf(DmfDownloadAndUpdateRequest.class) + .hasFieldOrPropertyWithValue("actionId", downloadActionId); + assertThat(cancelMessage.getAction()).isExactlyInstanceOf(DmfActionRequest.class) + .hasFieldOrPropertyWithValue("actionId", cancelActionId); + assertDmfDownloadAndUpdateRequest((DmfDownloadAndUpdateRequest) installMessage.getAction(), ds.getModules(), + controllerId); + assertDmfDownloadAndUpdateRequest((DmfDownloadAndUpdateRequest) downloadMessage.getAction(), ds.getModules(), + controllerId); + } + + private List getLatestMultiActionMessages(final String expectedControllerId) { + final Message multiactionMessage = replyToListener.getLatestEventMessage(EventTopic.MULTI_ACTION); + assertThat(multiactionMessage.getMessageProperties().getHeaders().get(MessageHeaderKey.THING_ID)) + .isEqualTo(expectedControllerId); + return ((DmfMultiActionRequest) getDmfClient().getMessageConverter().fromMessage(multiactionMessage)) + .getElements(); + } + @Test @Description("Handle cancelation process of an action in multi assignment mode.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @@ -197,8 +259,8 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer final String controllerId = TARGET_PREFIX + "cancelActionInMultiAssignMode"; registerAndAssertTargetWithExistingTenant(controllerId); - final long actionId1 = assignNewDsToTarget(controllerId); - final long actionId2 = assignNewDsToTarget(controllerId); + final long actionId1 = assignNewDsToTarget(controllerId, 675); + final long actionId2 = assignNewDsToTarget(controllerId, 343); waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION, EventTopic.MULTI_ACTION); deploymentManagement.cancelAction(actionId1); waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); @@ -229,8 +291,8 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer final String controllerId = TARGET_PREFIX + "finishActionInMultiAssignMode"; registerAndAssertTargetWithExistingTenant(controllerId); - final long actionId1 = assignNewDsToTarget(controllerId); - final long actionId2 = assignNewDsToTarget(controllerId); + final long actionId1 = assignNewDsToTarget(controllerId, 66); + final long actionId2 = assignNewDsToTarget(controllerId, 767); final Entry action2Install = new SimpleEntry<>(actionId2, EventTopic.DOWNLOAD_AND_INSTALL); waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION, EventTopic.MULTI_ACTION); @@ -254,11 +316,11 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer enableMultiAssignments(); final String controllerId = TARGET_PREFIX + "assignDsMultipleTimesInMultiAssignMode"; registerAndAssertTargetWithExistingTenant(controllerId); - final DistributionSet ds = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final Long dsId = testdataFactory.createDistributionSet().getId(); - final Long actionId1 = getFirstAssignedActionId(assignDistributionSet(ds.getId(), controllerId)); + final Long actionId1 = getFirstAssignedAction(assignDistributionSet(dsId, controllerId, 344)).getId(); waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); - final Long actionId2 = getFirstAssignedActionId(assignDistributionSet(ds.getId(), controllerId)); + final Long actionId2 = getFirstAssignedAction(assignDistributionSet(dsId, controllerId, 775)).getId(); waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); final Entry action1Install = new SimpleEntry<>(actionId1, EventTopic.DOWNLOAD_AND_INSTALL); @@ -272,8 +334,14 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer } private Long assignNewDsToTarget(final String controllerId) { - final DistributionSet ds = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); - final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds.getId(), controllerId)); + return assignNewDsToTarget(controllerId, null); + } + + private Long assignNewDsToTarget(final String controllerId, final Integer weight) { + final DistributionSet ds = testdataFactory.createDistributionSet(); + final Long actionId = getFirstAssignedActionId( + assignDistributionSet(ds.getId(), Collections.singletonList(controllerId), ActionType.FORCED, + RepositoryModelConstants.NO_FORCE_TIME, weight)); waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.PENDING); return actionId; } @@ -296,15 +364,15 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer final String controllerId = TARGET_PREFIX + "startRolloutsWithSameDsInMultiAssignMode"; registerAndAssertTargetWithExistingTenant(controllerId); - final DistributionSet ds = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final DistributionSet ds = testdataFactory.createDistributionSet(); final Set smIds = getSoftwareModuleIds(ds); final String filterQuery = "controllerId==" + controllerId; - createAndStartRollout(ds, filterQuery); + createAndStartRollout(ds, filterQuery, 122); waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); assertLatestMultiActionMessageContainsInstallMessages(controllerId, Arrays.asList(smIds)); - createAndStartRollout(ds, filterQuery); + createAndStartRollout(ds, filterQuery, 43); waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); assertLatestMultiActionMessageContainsInstallMessages(controllerId, Arrays.asList(smIds, smIds)); } @@ -327,15 +395,15 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer registerAndAssertTargetWithExistingTenant(controllerId); final String filterQuery = "controllerId==" + controllerId; - final DistributionSet ds1 = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final DistributionSet ds1 = testdataFactory.createDistributionSet(); final Set smIds1 = getSoftwareModuleIds(ds1); - final DistributionSet ds2 = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final DistributionSet ds2 = testdataFactory.createDistributionSet(); final Set smIds2 = getSoftwareModuleIds(ds2); - createAndStartRollout(ds1, filterQuery); - createAndStartRollout(ds2, filterQuery); + createAndStartRollout(ds1, filterQuery, 12); + createAndStartRollout(ds2, filterQuery, 45); waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION, EventTopic.MULTI_ACTION); - createAndStartRollout(ds1, filterQuery); + createAndStartRollout(ds1, filterQuery, 65); waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); assertLatestMultiActionMessageContainsInstallMessages(controllerId, Arrays.asList(smIds1, smIds2, smIds1)); @@ -357,8 +425,12 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer } private Rollout createAndStartRollout(final DistributionSet ds, final String filterQuery) { + return createAndStartRollout(ds, filterQuery, null); + } + + private Rollout createAndStartRollout(final DistributionSet ds, final String filterQuery, final Integer weight) { final Rollout rollout = testdataFactory.createRolloutByVariables(UUID.randomUUID().toString(), "", 1, - filterQuery, ds, "50", "5"); + filterQuery, ds, "50", "5", ActionType.FORCED, weight); rolloutManagement.start(rollout.getId()); rolloutManagement.handleRollouts(); return rollout; @@ -483,11 +555,7 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer } private List> getLatestMultiActionMessageActions(final String expectedControllerId) { - final Message multiactionMessage = replyToListener.getLatestEventMessage(EventTopic.MULTI_ACTION); - assertThat(multiactionMessage.getMessageProperties().getHeaders().get(MessageHeaderKey.THING_ID)) - .isEqualTo(expectedControllerId); - final List multiActionRequest = ((DmfMultiActionRequest) getDmfClient() - .getMessageConverter().fromMessage(multiactionMessage)).getElements(); + final List multiActionRequest = getLatestMultiActionMessages(expectedControllerId); return multiActionRequest.stream() .map(request -> new SimpleEntry<>(request.getAction().getActionId(), request.getTopic())) .collect(Collectors.toList()); diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfMultiActionRequest.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfMultiActionRequest.java index 9f98d331c..6e0021cd9 100644 --- a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfMultiActionRequest.java +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfMultiActionRequest.java @@ -52,10 +52,11 @@ public class DmfMultiActionRequest { elements.add(element); } - public void addElement(final EventTopic topic, final DmfActionRequest action) { + public void addElement(final EventTopic topic, final DmfActionRequest action, final int weight) { final DmfMultiActionElement element = new DmfMultiActionElement(); element.setTopic(topic); element.setAction(action); + element.setWeight(weight); addElement(element); } @@ -70,6 +71,9 @@ public class DmfMultiActionRequest { @JsonProperty private DmfActionRequest action; + @JsonProperty + private int weight; + public DmfActionRequest getAction() { return action; } @@ -89,6 +93,13 @@ public class DmfMultiActionRequest { this.topic = actionType; } + public void setWeight(final int weight) { + this.weight = weight; + } + + public int getWeight() { + return weight; + } } } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java index 28e314f28..562415c23 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java @@ -46,13 +46,13 @@ import org.springframework.security.access.prepost.PreAuthorize; public interface ControllerManagement { /** - * Adds an {@link ActionStatus} for a cancel {@link Action} including potential - * state changes for the target and the {@link Action} itself. + * Adds an {@link ActionStatus} for a cancel {@link Action} including + * potential state changes for the target and the {@link Action} itself. * * @param create * to be added * @return the updated {@link Action} - * + * * @throws EntityAlreadyExistsException * if a given entity already exists * @@ -93,8 +93,8 @@ public interface ControllerManagement { @NotNull Collection moduleId); /** - * Simple addition of a new {@link ActionStatus} entry to the {@link Action} . - * No state changes. + * Simple addition of a new {@link ActionStatus} entry to the + * {@link Action}. No state changes. * * @param create * to add to the action @@ -136,36 +136,48 @@ public interface ControllerManagement { Action addUpdateActionStatus(@NotNull @Valid ActionStatusCreate create); /** - * Retrieves oldest {@link Action} that is active and assigned to a - * {@link Target}. - * + * Retrieves active {@link Action} with highest priority that is assigned to + * a {@link Target}. + * * For performance reasons this method does not throw - * {@link EntityNotFoundException} in case target with given controlelrId + * {@link EntityNotFoundException} in case target with given controllerId * does not exist but will return an {@link Optional#empty()} instead. * * @param controllerId - * identifies the target to retrieve the actions from - * @return a list of actions assigned to given target which are active - * + * identifies the target to retrieve the action from + * @return the action + * */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) - Optional findOldestActiveActionByTarget(@NotEmpty String controllerId); + Optional findActiveActionWithHighestWeight(@NotEmpty String controllerId); /** - * Retrieves all active actions which are assigned to the target with the given - * controller ID. - * - * @param pageable - * pagination parameter + * Retrieves active {@link Action}s with highest weight that are assigned to + * a {@link Target}. + * * @param controllerId - * of the target - * @return the requested {@link Page} with {@link Action}s + * identifies the target to retrieve the action from + * @param maxActionCount + * max size of returned list + * @return the action + * */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) - Page findActiveActionsByTarget(@NotNull Pageable pageable, @NotEmpty String controllerId); + List findActiveActionsWithHighestWeight(@NotEmpty String controllerId, int maxActionCount); /** - * Get the {@link Action} entity for given actionId with all lazy attributes. + * 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 + */ + int getWeightConsideringDefault(final Action action); + + /** + * Get the {@link Action} entity for given actionId with all lazy + * attributes. * * @param actionId * to be id of the action @@ -175,7 +187,8 @@ public interface ControllerManagement { Optional findActionWithDetails(long actionId); /** - * Retrieves all the {@link ActionStatus} entries of the given {@link Action}. + * Retrieves all the {@link ActionStatus} entries of the given + * {@link Action}. * * @param pageReq * pagination parameter @@ -190,10 +203,11 @@ public interface ControllerManagement { Page findActionStatusByAction(@NotNull Pageable pageReq, long actionId); /** - * Register new target in the repository (plug-and-play) and in case it already - * exists updates {@link Target#getAddress()} and + * Register new target in the repository (plug-and-play) and in case it + * already exists updates {@link Target#getAddress()} and * {@link Target#getLastTargetQuery()} and switches if - * {@link TargetUpdateStatus#UNKNOWN} to {@link TargetUpdateStatus#REGISTERED}. + * {@link TargetUpdateStatus#UNKNOWN} to + * {@link TargetUpdateStatus#REGISTERED}. * * @param controllerId * reference @@ -205,10 +219,11 @@ public interface ControllerManagement { Target findOrRegisterTargetIfItDoesNotExist(@NotEmpty String controllerId, @NotNull URI address); /** - * Register new target in the repository (plug-and-play) and in case it already - * exists updates {@link Target#getAddress()} and - * {@link Target#getLastTargetQuery()} and {@link Target#getName()} and switches if - * {@link TargetUpdateStatus#UNKNOWN} to {@link TargetUpdateStatus#REGISTERED}. + * Register new target in the repository (plug-and-play) and in case it + * already exists updates {@link Target#getAddress()} and + * {@link Target#getLastTargetQuery()} and {@link Target#getName()} and + * switches if {@link TargetUpdateStatus#UNKNOWN} to + * {@link TargetUpdateStatus#REGISTERED}. * * @param controllerId * reference @@ -222,14 +237,14 @@ public interface ControllerManagement { Target findOrRegisterTargetIfItDoesNotExist(@NotEmpty String controllerId, @NotNull URI address, String name); /** - * Retrieves last {@link Action} for a download of an artifact of given module - * and target if exists and is not canceled. + * Retrieves last {@link Action} for a download of an artifact of given + * module and target if exists and is not canceled. * * @param controllerId * to look for * @param moduleId - * of the the {@link SoftwareModule} that should be assigned to the - * target + * of the the {@link SoftwareModule} that should be assigned to + * the target * @return last {@link Action} for given combination * * @throws EntityNotFoundException @@ -267,18 +282,18 @@ public interface ControllerManagement { int getMaintenanceWindowPollCount(); /** - * Returns polling time based on the maintenance window for an action. Server - * will reduce the polling interval as the start time for maintenance window - * approaches, so that at least these many attempts are made between current - * polling until start of maintenance window. Poll time keeps reducing with - * MinPollingTime as lower limit - * {@link TenantConfigurationKey#MIN_POLLING_TIME_INTERVAL}. After the start of - * maintenance window, it resets to default + * Returns polling time based on the maintenance window for an action. + * Server will reduce the polling interval as the start time for maintenance + * window approaches, so that at least these many attempts are made between + * current polling until start of maintenance window. Poll time keeps + * reducing with MinPollingTime as lower limit + * {@link TenantConfigurationKey#MIN_POLLING_TIME_INTERVAL}. After the start + * of maintenance window, it resets to default * {@link TenantConfigurationKey#POLLING_TIME_INTERVAL}. * * @param actionId - * id the {@link Action} for which polling time is calculated based - * on it having maintenance window or not + * id the {@link Action} for which polling time is calculated + * based on it having maintenance window or not * * @return current {@link TenantConfigurationKey#POLLING_TIME_INTERVAL}. */ @@ -286,20 +301,20 @@ public interface ControllerManagement { String getPollingTimeForAction(long actionId); /** - * Checks if a given target has currently or has even been assigned to the given - * artifact through the action history list. This can e.g. indicate if a target - * is allowed to download a given artifact because it has currently assigned or - * had ever been assigned to the target and so it's visible to a specific target - * e.g. for downloading. + * Checks if a given target has currently or has even been assigned to the + * given artifact through the action history list. This can e.g. indicate if + * a target is allowed to download a given artifact because it has currently + * assigned or had ever been assigned to the target and so it's visible to a + * specific target e.g. for downloading. * * @param controllerId * the ID of the target to check * @param sha1Hash * of the artifact to verify if the given target had even been * assigned to - * @return {@code true} if the given target has currently or had ever a relation - * to the given artifact through the action history, otherwise - * {@code false} + * @return {@code true} if the given target has currently or had ever a + * relation to the given artifact through the action history, + * otherwise {@code false} * * @throws EntityNotFoundException * if target with given ID does not exist @@ -308,20 +323,20 @@ public interface ControllerManagement { boolean hasTargetArtifactAssigned(@NotEmpty String controllerId, @NotEmpty String sha1Hash); /** - * Checks if a given target has currently or has even been assigned to the given - * artifact through the action history list. This can e.g. indicate if a target - * is allowed to download a given artifact because it has currently assigned or - * had ever been assigned to the target and so it's visible to a specific target - * e.g. for downloading. + * Checks if a given target has currently or has even been assigned to the + * given artifact through the action history list. This can e.g. indicate if + * a target is allowed to download a given artifact because it has currently + * assigned or had ever been assigned to the target and so it's visible to a + * specific target e.g. for downloading. * * @param targetId * the ID of the target to check * @param sha1Hash * of the artifact to verify if the given target had even been * assigned to - * @return {@code true} if the given target has currently or had ever a relation - * to the given artifact through the action history, otherwise - * {@code false} + * @return {@code true} if the given target has currently or had ever a + * relation to the given artifact through the action history, + * otherwise {@code false} * * @throws EntityNotFoundException * if target with given ID does not exist @@ -330,8 +345,8 @@ public interface ControllerManagement { boolean hasTargetArtifactAssigned(long targetId, @NotEmpty String sha1Hash); /** - * Registers retrieved status for given {@link Target} and {@link Action} if it - * does not exist yet. + * Registers retrieved status for given {@link Target} and {@link Action} if + * it does not exist yet. * * @param actionId * to the handle status for @@ -385,8 +400,9 @@ public interface ControllerManagement { Optional getByControllerId(@NotEmpty String controllerId); /** - * Finds {@link Target} based on given ID returns found Target without details, - * i.e. NO {@link Target#getTags()} and {@link Target#getActions()} possible. + * Finds {@link Target} based on given ID returns found Target without + * details, i.e. NO {@link Target#getTags()} and {@link Target#getActions()} + * possible. * * @param targetId * to look for. @@ -398,20 +414,21 @@ public interface ControllerManagement { Optional get(long targetId); /** - * Retrieves the specified number of messages from action history of the given - * {@link Action} based on messageCount. Regardless of the value of + * Retrieves the specified number of messages from action history of the + * given {@link Action} based on messageCount. Regardless of the value of * messageCount, in order to restrict resource utilisation by controllers, * maximum number of messages that are retrieved from database is limited by - * {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}. messageCount less - * then zero, retrieves the maximum allowed number of action status messages - * from history; messageCount equal zero, does not retrieve any message; and - * messageCount larger then zero, retrieves the specified number of messages, - * limited by maximum allowed number. A controller sends the feedback for an - * {@link ActionStatus} as a list of messages; while returning the messages, - * even though the messages from multiple {@link ActionStatus} are retrieved in - * descending order by the reported time ({@link ActionStatus#getOccurredAt()}), - * i.e. latest ActionStatus first, the sub-ordering of messages from within - * single {@link ActionStatus} is unspecified. + * {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}. messageCount + * less then zero, retrieves the maximum allowed number of action status + * messages from history; messageCount equal zero, does not retrieve any + * message; and messageCount larger then zero, retrieves the specified + * number of messages, limited by maximum allowed number. A controller sends + * the feedback for an {@link ActionStatus} as a list of messages; while + * returning the messages, even though the messages from multiple + * {@link ActionStatus} are retrieved in descending order by the reported + * time ({@link ActionStatus#getOccurredAt()}), i.e. latest ActionStatus + * first, the sub-ordering of messages from within single + * {@link ActionStatus} is unspecified. * * @param actionId * to be filtered on @@ -424,10 +441,10 @@ public interface ControllerManagement { List getActionHistoryMessages(long actionId, int messageCount); /** - * Cancels given {@link Action} for this {@link Target}. However, it might be - * possible that the controller will continue to work on the cancellation. The - * controller needs to acknowledge or reject the cancellation using - * {@link DdiRootController#postCancelActionFeedback}. + * Cancels given {@link Action} for this {@link Target}. However, it might + * be possible that the controller will continue to work on the + * cancellation. The controller needs to acknowledge or reject the + * cancellation using {@link DdiRootController#postCancelActionFeedback}. * * @param actionId * to be canceled @@ -454,8 +471,9 @@ public interface ControllerManagement { void updateActionExternalRef(long actionId, @NotEmpty String externalRef); /** - * Retrieves list of {@link Action}s which matches the provided externalRefs. - * + * Retrieves list of {@link Action}s which matches the provided + * externalRefs. + * * @param externalRefs * for which the actions need to be fetched. * @return list of {@link Action}s matching the externalRefs. 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 32cfda0f1..27d009e08 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 @@ -14,6 +14,7 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.Set; +import javax.validation.Valid; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; @@ -76,7 +77,7 @@ public interface DeploymentManagement { */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) List assignDistributionSets( - @NotEmpty List deploymentRequests); + @Valid @NotEmpty List deploymentRequests); /** * Assigns {@link DistributionSet}s to {@link Target}s according to the @@ -107,9 +108,9 @@ public interface DeploymentManagement { */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) List assignDistributionSets( - @NotEmpty List deploymentRequests, String actionMessage); - - /** + @Valid @NotEmpty List deploymentRequests, String actionMessage); + + /** * build a {@link DeploymentRequest} for a target distribution set * assignment * @@ -372,6 +373,30 @@ public interface DeploymentManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) Page findInActiveActionsByTarget(@NotNull Pageable pageable, @NotEmpty String controllerId); + /** + * 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); + + /** + * 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 + */ + int getWeightConsideringDefault(final Action action); + /** * Force cancels given {@link Action} for given {@link Target}. Force * canceling means that the action is marked as canceled on the SP server 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 46e563cda..771707554 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 @@ -27,8 +27,8 @@ public class RepositoryProperties { * especially useful if the action status feedback channel order from the * device cannot be guaranteed. * - * Note: if this is enforced you have to make sure that the feedback - * channel from the devices is in order. + * Note: if this is enforced you have to make sure that the feedback channel + * from the devices is in order. */ private boolean rejectActionStatusForClosedAction; @@ -54,6 +54,11 @@ public class RepositoryProperties { */ private boolean eagerPollPersistence; + /** + * If an {@link Action} has a weight of null this value is used as weight. + */ + private int actionWeightIfAbsent = 1000; + public boolean isEagerPollPersistence() { return eagerPollPersistence; } @@ -94,4 +99,12 @@ public class RepositoryProperties { this.publishTargetPollEvent = publishTargetPollEvent; } + public int getActionWeightIfAbsent() { + return actionWeightIfAbsent; + } + + public void setActionWeightIfAbsent(final int actionWeightIfAbsent) { + this.actionWeightIfAbsent = actionWeightIfAbsent; + } + } 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 e96f73d07..393bc0ca1 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 @@ -129,7 +129,7 @@ public interface RolloutManagement { * exceeded. */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_CREATE) - Rollout create(@NotNull RolloutCreate create, int amountGroup, @NotNull RolloutGroupConditions conditions); + Rollout create(@NotNull @Valid RolloutCreate create, int amountGroup, @NotNull RolloutGroupConditions conditions); /** * Persists a new rollout entity. The filter within the @@ -166,7 +166,7 @@ public interface RolloutManagement { * exceeded. */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_CREATE) - Rollout create(@NotNull @Valid RolloutCreate rollout, @NotNull @Valid List groups, + Rollout create(@Valid @NotNull RolloutCreate rollout, @NotNull @Valid List groups, RolloutGroupConditions conditions); /** @@ -342,9 +342,9 @@ public interface RolloutManagement { /** * Approves or denies a created rollout being in state - * {@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} + * {@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. @@ -365,9 +365,9 @@ public interface RolloutManagement { /** * Approves or denies a created rollout being in state - * {@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} + * {@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. 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 937f9fb4f..fd5e2fb42 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 @@ -15,6 +15,7 @@ import javax.validation.Valid; import javax.validation.constraints.NotNull; 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; @@ -208,7 +209,7 @@ public interface TargetFilterQueryManagement { * if fields are not filled as specified. Check * {@link TargetFilterQueryUpdate} for field constraints. * - * @throws AssignmentQuotaExceededException + * @throws QuotaExceededException * if the update contains a new query which addresses too many * targets (auto-assignments only) */ @@ -216,46 +217,10 @@ public interface TargetFilterQueryManagement { TargetFilterQuery update(@NotNull @Valid TargetFilterQueryUpdate update); /** - * Updates the the auto-assign {@link DistributionSet} and sets default - * (FORCED) {@link ActionType} of the addressed {@link TargetFilterQuery}. - * - * @param queryId - * of the target filter query to be updated - * @param dsId - * to be updated or null in order to remove it - * together with the auto-assign {@link ActionType} + * Updates the auto assign settings of an {@link TargetFilterQuery}. * - * @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 InvalidAutoAssignDistributionSetException - * if the provided auto-assign {@link DistributionSet} is not - * valid (incomplete or soft deleted) - */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) - default TargetFilterQuery updateAutoAssignDS(final long queryId, final Long dsId) { - return updateAutoAssignDSWithActionType(queryId, dsId, null); - } - - /** - * Updates the the auto-assign {@link DistributionSet} and - * {@link ActionType} of the addressed {@link TargetFilterQuery}. - * - * @param queryId - * of the target filter query to be updated - * @param dsId - * to be updated or null in order to remove it - * together with the auto-assign {@link ActionType} - * @param actionType - * to be updated or null for default (FORCED) if - * distribution set Id is present + * @param autoAssignDistributionSetUpdate + * the new auto assignment * * @return the updated {@link TargetFilterQuery} * @@ -276,5 +241,6 @@ public interface TargetFilterQueryManagement { * valid (incomplete or soft deleted) */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) - TargetFilterQuery updateAutoAssignDSWithActionType(long queryId, Long dsId, ActionType actionType); + TargetFilterQuery updateAutoAssignDS( + @NotNull @Valid AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate); } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/AutoAssignDistributionSetUpdate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/AutoAssignDistributionSetUpdate.java new file mode 100644 index 000000000..d80f96991 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/AutoAssignDistributionSetUpdate.java @@ -0,0 +1,94 @@ +/** + * 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.builder; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; + +/** + * Builder to update the auto assign {@link DistributionSet} of a + * {@link TargetFilterQuery} entry. Defines all fields that can be updated. + */ +public class AutoAssignDistributionSetUpdate { + private final long targetFilterId; + private Long dsId; + private ActionType actionType; + + @Min(Action.WEIGHT_MIN) + @Max(Action.WEIGHT_MAX) + private Integer weight; + + /** + * Constructor + * + * @param targetFilterId + * ID of {@link TargetFilterQuery} to update + */ + public AutoAssignDistributionSetUpdate(final long targetFilterId) { + this.targetFilterId = targetFilterId; + } + + /** + * Specify {@link DistributionSet} + * + * @param dsId + * ID of the {@link DistributionSet} + * @return updated builder instance + */ + public AutoAssignDistributionSetUpdate ds(final Long dsId) { + this.dsId = dsId; + return this; + } + + /** + * Specify {@link DistributionSet} + * + * @param actionType + * {@link ActionType} used for the auto assignment + * @return updated builder instance + */ + public AutoAssignDistributionSetUpdate actionType(final ActionType actionType) { + this.actionType = actionType; + return this; + } + + /** + * Specify weight of resulting {@link Action} + * + * @param weight + * weight used for the auto assignment + * @return updated builder instance + */ + public AutoAssignDistributionSetUpdate weight(final Integer weight) { + this.weight = weight; + return this; + } + + public Long getDsId() { + return dsId; + } + + public ActionType getActionType() { + return actionType; + } + + public Integer getWeight() { + return weight; + } + + public long getTargetFilterId() { + return targetFilterId; + } + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutCreate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutCreate.java index 42e76f73f..f8cd2e6b2 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutCreate.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutCreate.java @@ -28,6 +28,8 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery; */ public interface RolloutCreate { /** + * Set name + * * @param name * for {@link Rollout#getName()} * @return updated builder instance @@ -35,6 +37,8 @@ public interface RolloutCreate { RolloutCreate name(@Size(min = 1, max = NamedEntity.NAME_MAX_SIZE) @NotNull String name); /** + * Set description + * * @param description * for {@link Rollout#getDescription()} * @return updated builder instance @@ -42,6 +46,8 @@ public interface RolloutCreate { RolloutCreate description(@Size(max = NamedEntity.DESCRIPTION_MAX_SIZE) String description); /** + * Set the {@link DistributionSet} + * * @param set * for {@link Rollout#getDistributionSet()} * @return updated builder instance @@ -51,6 +57,8 @@ public interface RolloutCreate { } /** + * Set the id of the {@link DistributionSet} + * * @param setId * for {@link Rollout#getDistributionSet()} * @return updated builder instance @@ -58,6 +66,8 @@ public interface RolloutCreate { RolloutCreate set(long setId); /** + * Set the {@link TargetFilterQuery} + * * @param targetFilterQuery * for {@link Rollout#getTargetFilterQuery()} * @return updated builder instance @@ -66,6 +76,8 @@ public interface RolloutCreate { @Size(min = 1, max = TargetFilterQuery.QUERY_MAX_SIZE) @NotNull String targetFilterQuery); /** + * Set the {@link ActionType} + * * @param actionType * for {@link Rollout#getActionType()} * @return updated builder instance @@ -73,6 +85,8 @@ public interface RolloutCreate { RolloutCreate actionType(@NotNull ActionType actionType); /** + * Set the forcedTime of the resulting {@link Actions} + * * @param forcedTime * for {@link Rollout#getForcedTime()} * @return updated builder instance @@ -80,6 +94,17 @@ public interface RolloutCreate { RolloutCreate forcedTime(Long forcedTime); /** + * Set the weight of the resulting {@link Actions} + * + * @param weight + * for {@link Rollout#getWeight()} + * @return updated builder instance + */ + RolloutCreate weight(Integer weight); + + /** + * set start + * * @param startAt * for {@link Rollout#getStartAt()} * @return updated builder instance diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutUpdate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutUpdate.java index b9ecfee41..8c1a768ab 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutUpdate.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutUpdate.java @@ -12,6 +12,7 @@ import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.NamedEntity; import org.eclipse.hawkbit.repository.model.Rollout; @@ -22,6 +23,8 @@ import org.eclipse.hawkbit.repository.model.Rollout; */ public interface RolloutUpdate { /** + * Set name of the {@link Rollout} + * * @param name * for {@link Rollout#getName()} * @return updated builder instance @@ -29,6 +32,8 @@ public interface RolloutUpdate { RolloutUpdate name(@Size(min = 1, max = NamedEntity.NAME_MAX_SIZE) @NotNull String name); /** + * Set description of the {@link Rollout} + * * @param description * for {@link Rollout#getDescription()} * @return updated builder instance @@ -36,6 +41,8 @@ public interface RolloutUpdate { RolloutUpdate description(@Size(max = NamedEntity.DESCRIPTION_MAX_SIZE) String description); /** + * Set ID of {@link DistributionSet} of the {@link Rollout} + * * @param setId * for {@link Rollout#getDistributionSet()} * @return updated builder instance @@ -43,6 +50,8 @@ public interface RolloutUpdate { RolloutUpdate set(long setId); /** + * Set action type of the {@link Rollout} + * * @param actionType * for {@link Rollout#getActionType()} * @return updated builder instance @@ -50,6 +59,8 @@ public interface RolloutUpdate { RolloutUpdate actionType(@NotNull Action.ActionType actionType); /** + * Set forcedTime of the {@link Rollout} + * * @param forcedTime * for {@link Rollout#getForcedTime()} * @return updated builder instance @@ -57,10 +68,20 @@ public interface RolloutUpdate { RolloutUpdate forcedTime(Long forcedTime); /** + * Set weight of {@link Action}s created by the {@link Rollout} + * + * @param weight + * for {@link Rollout#getWeight()} + * @return updated builder instance + */ + RolloutUpdate weight(Integer weight); + + /** + * Set start time of the {@link Rollout} + * * @param startAt * for {@link Rollout#getStartAt()} * @return updated builder instance */ RolloutUpdate startAt(Long startAt); - } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetFilterQueryBuilder.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetFilterQueryBuilder.java index 2e9698743..8dd14bdc8 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetFilterQueryBuilder.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetFilterQueryBuilder.java @@ -16,12 +16,23 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery; */ public interface TargetFilterQueryBuilder { /** + * Used to update a {@link TargetFilterQuery} + * * @param id * of the updatable entity * @return builder instance */ TargetFilterQueryUpdate update(long id); + /** + * Used to update a the auto assignment of a {@link TargetFilterQuery} + * + * @param id + * of the updatable entity + * @return builder instance + */ + AutoAssignDistributionSetUpdate updateAutoAssign(long id); + /** * @return builder instance */ diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetFilterQueryCreate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetFilterQueryCreate.java index 187bfbf14..4d8b42474 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetFilterQueryCreate.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetFilterQueryCreate.java @@ -13,6 +13,7 @@ import java.util.Optional; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.BaseEntity; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -27,6 +28,8 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery; */ public interface TargetFilterQueryCreate { /** + * Set filter name + * * @param name * of {@link TargetFilterQuery#getName()} * @return updated builder instance @@ -34,6 +37,8 @@ public interface TargetFilterQueryCreate { TargetFilterQueryCreate name(@Size(min = 1, max = NamedEntity.NAME_MAX_SIZE) @NotNull String name); /** + * Set filter query + * * @param query * of {@link TargetFilterQuery#getQuery()} * @return updated builder instance @@ -41,6 +46,8 @@ public interface TargetFilterQueryCreate { TargetFilterQueryCreate query(@Size(min = 1, max = TargetFilterQuery.QUERY_MAX_SIZE) @NotNull String query); /** + * Set {@link DistributionSet} for auto assignment + * * @param distributionSet * for {@link TargetFilterQuery#getAutoAssignDistributionSet()} * @return updated builder instance @@ -50,6 +57,8 @@ public interface TargetFilterQueryCreate { } /** + * Set ID of {@link DistributionSet} for auto assignment + * * @param dsId * for {@link TargetFilterQuery#getAutoAssignDistributionSet()} * @return updated builder instance @@ -57,12 +66,23 @@ public interface TargetFilterQueryCreate { TargetFilterQueryCreate autoAssignDistributionSet(Long dsId); /** + * Set {@link ActionType} for auto assignment + * * @param actionType * for {@link TargetFilterQuery#getAutoAssignActionType()} * @return updated builder instance */ TargetFilterQueryCreate autoAssignActionType(ActionType actionType); + /** + * Set weight of {@link Action} created during auto assignment + * + * @param weight + * weight of {@link Action} generated within auto assignment + * @return updated builder instance + */ + TargetFilterQueryCreate autoAssignWeight(Integer weight); + /** * @return peek on current state of {@link TargetFilterQuery} in the builder */ diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetFilterQueryUpdate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetFilterQueryUpdate.java index 15dbef687..601f62715 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetFilterQueryUpdate.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetFilterQueryUpdate.java @@ -32,5 +32,4 @@ public interface TargetFilterQueryUpdate { * @return updated builder instance */ TargetFilterQueryUpdate query(@Size(min = 1, max = TargetFilterQuery.QUERY_MAX_SIZE) String query); - } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/NoWeightProvidedInMultiAssignmentModeException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/NoWeightProvidedInMultiAssignmentModeException.java new file mode 100644 index 000000000..2c3758301 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/NoWeightProvidedInMultiAssignmentModeException.java @@ -0,0 +1,63 @@ +/** + * 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; + +/** + * This exception is thrown if multi assignments is enabled and an target + * distribution set assignment request does not contain a weight value + * + */ +public class NoWeightProvidedInMultiAssignmentModeException extends AbstractServerRtException { + + + private static final long serialVersionUID = 1L; + private static final SpServerError THIS_ERROR = SpServerError.SP_NO_WEIGHT_PROVIDED_IN_MULTIASSIGNMENT_MODE; + + /** + * Default constructor. + */ + public NoWeightProvidedInMultiAssignmentModeException() { + super(THIS_ERROR); + } + + /** + * Parameterized constructor. + * + * @param cause + * of the exception + */ + public NoWeightProvidedInMultiAssignmentModeException(final Throwable cause) { + super(THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + * @param cause + * of the exception + */ + public NoWeightProvidedInMultiAssignmentModeException(final String message, final Throwable cause) { + super(message, THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + */ + public NoWeightProvidedInMultiAssignmentModeException(final String message) { + super(message, THIS_ERROR); + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java index 30c2ef475..1c6f2e568 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java @@ -38,7 +38,16 @@ public interface Action extends TenantAwareBaseEntity { * Maximum length of external reference. */ int EXTERNAL_REF_MAX_LENGTH = 512; - + + /** + * Minimum weight to indicate the priority of {@link Action}. + */ + int WEIGHT_MIN = 0; + /** + * Maximum weight to indicate the priority of {@link Action}. + */ + int WEIGHT_MAX = 1000; + /** * @return the distributionSet */ @@ -80,6 +89,11 @@ public interface Action extends TenantAwareBaseEntity { */ long getForcedTime(); + /** + * @return priority of the {@link Action}. + */ + Optional getWeight(); + /** * @return rolloutGroup related to this {@link Action}. */ @@ -106,10 +120,11 @@ public interface Action extends TenantAwareBaseEntity { String getMaintenanceWindowTimeZone(); /** - * @param externalRef associated with this action + * @param externalRef + * associated with this action */ void setExternalRef(@NotEmpty String externalRef); - + /** * @return externalRef of the action */ diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DeploymentRequest.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DeploymentRequest.java index b1c00aa24..df0ee59c6 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DeploymentRequest.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DeploymentRequest.java @@ -10,6 +10,8 @@ package org.eclipse.hawkbit.repository.model; import java.util.Objects; +import javax.validation.Valid; + import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException; import org.eclipse.hawkbit.repository.model.Action.ActionType; @@ -19,6 +21,7 @@ import org.eclipse.hawkbit.repository.model.Action.ActionType; */ public class DeploymentRequest { private final Long distributionSetId; + @Valid private final TargetWithActionType targetWithActionType; /** @@ -34,6 +37,8 @@ public class DeploymentRequest { * specified for the action. * @param forceTime * at what time the type soft turns into forced. + * @param weight + * the priority of an {@link Action}. * @param maintenanceSchedule * is the cron expression to be used for scheduling maintenance * windows. Expression has 6 mandatory fields and 1 last optional @@ -51,11 +56,10 @@ public class DeploymentRequest { * if the parameters do not define a valid maintenance schedule. */ public DeploymentRequest(final String controllerId, final Long distributionSetId, final ActionType actionType, - final long forceTime, final String maintenanceSchedule, final String maintenanceWindowDuration, - final String maintenanceWindowTimeZone) { - this.targetWithActionType = new TargetWithActionType(controllerId, actionType, forceTime, maintenanceSchedule, - maintenanceWindowDuration, - maintenanceWindowTimeZone); + final long forceTime, final Integer weight, final String maintenanceSchedule, + final String maintenanceWindowDuration, final String maintenanceWindowTimeZone) { + this.targetWithActionType = new TargetWithActionType(controllerId, actionType, forceTime, weight, + maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone); this.distributionSetId = distributionSetId; } @@ -71,14 +75,13 @@ public class DeploymentRequest { return targetWithActionType; } - @Override public String toString() { return String.format( - "DeploymentRequest [controllerId=%s, distributionSetId=%d, actionType=%s, forceTime=%d, maintenanceSchedule=%s, maintenanceWindowDuration=%s, maintenanceWindowTimeZone=%s]", + "DeploymentRequest [controllerId=%s, distributionSetId=%d, actionType=%s, forceTime=%d, weight=%d, maintenanceSchedule=%s, maintenanceWindowDuration=%s, maintenanceWindowTimeZone=%s]", targetWithActionType.getControllerId(), getDistributionSetId(), targetWithActionType.getActionType(), - targetWithActionType.getForceTime(), targetWithActionType.getMaintenanceSchedule(), - targetWithActionType.getMaintenanceWindowDuration(), + targetWithActionType.getForceTime(), targetWithActionType.getWeight(), + targetWithActionType.getMaintenanceSchedule(), targetWithActionType.getMaintenanceWindowDuration(), targetWithActionType.getMaintenanceWindowTimeZone()); } @@ -86,7 +89,7 @@ public class DeploymentRequest { public int hashCode() { return Objects.hash(distributionSetId, targetWithActionType); } - + @Override public boolean equals(final Object obj) { if (this == obj) { diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DeploymentRequestBuilder.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DeploymentRequestBuilder.java index 8386ef730..268a1bd70 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DeploymentRequestBuilder.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DeploymentRequestBuilder.java @@ -18,7 +18,7 @@ public class DeploymentRequestBuilder { private final String controllerId; private final Long distributionSetId; - + private Integer weight; private long forceTime = RepositoryModelConstants.NO_FORCE_TIME; private ActionType actionType = ActionType.FORCED; private String maintenanceSchedule; @@ -63,6 +63,18 @@ public class DeploymentRequestBuilder { return this; } + /** + * Set the weight of the action. + * + * @param weight + * the priority given to the action. + * @return builder + */ + public DeploymentRequestBuilder setWeight(final Integer weight) { + this.weight = weight; + return this; + } + /** * Set a maintenanceWindow * @@ -94,8 +106,8 @@ public class DeploymentRequestBuilder { * @return the request object */ public DeploymentRequest build() { - return new DeploymentRequest(controllerId, distributionSetId, actionType, forceTime, maintenanceSchedule, - maintenanceWindowDuration, maintenanceWindowTimeZone); + return new DeploymentRequest(controllerId, distributionSetId, actionType, forceTime, weight, + maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone); } } 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 5fac43173..7e1c50e2c 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 @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.repository.model; +import java.util.Optional; import java.util.concurrent.TimeUnit; import org.eclipse.hawkbit.repository.model.Action.ActionType; @@ -101,6 +102,11 @@ public interface Rollout extends NamedEntity { */ String getApprovalRemark(); + /** + * @return the priority of {@link Rollout}. + */ + Optional getWeight(); + /** * * State machine for rollout. @@ -197,5 +203,4 @@ public interface Rollout extends NamedEntity { */ DENIED } - } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java index 987880a33..e4b51e47a 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.repository.model; import java.util.Collections; import java.util.EnumSet; +import java.util.Optional; import java.util.Set; import org.eclipse.hawkbit.repository.model.Action.ActionType; @@ -70,4 +71,10 @@ public interface TargetFilterQuery extends TenantAwareBaseEntity { */ ActionType getAutoAssignActionType(); + /** + * @return the weight of the {@link Action}s created during an auto + * assignment. + */ + Optional getAutoAssignWeight(); + } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetWithActionType.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetWithActionType.java index ac48f3849..847861636 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetWithActionType.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetWithActionType.java @@ -10,6 +10,9 @@ package org.eclipse.hawkbit.repository.model; import java.util.Objects; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException; import org.eclipse.hawkbit.repository.model.Action.ActionType; @@ -21,6 +24,9 @@ public class TargetWithActionType { private final String controllerId; private final ActionType actionType; private final long forceTime; + @Min(Action.WEIGHT_MIN) + @Max(Action.WEIGHT_MAX) + private final Integer weight; private String maintenanceSchedule; private String maintenanceWindowDuration; private String maintenanceWindowTimeZone; @@ -32,7 +38,7 @@ public class TargetWithActionType { * ID if the controller */ public TargetWithActionType(final String controllerId) { - this(controllerId, ActionType.FORCED, 0); + this(controllerId, ActionType.FORCED, 0, null); } /** @@ -43,13 +49,17 @@ public class TargetWithActionType { * @param actionType * specified for the action. * @param forceTime - * after that point in time the action is exposed as forcen in + * after that point in time the action is exposed as forced in * case the type is {@link ActionType#TIMEFORCED} + * @param weight + * the priority of an {@link Action} */ - public TargetWithActionType(final String controllerId, final ActionType actionType, final long forceTime) { + public TargetWithActionType(final String controllerId, final ActionType actionType, final long forceTime, + final Integer weight) { this.controllerId = controllerId; this.actionType = actionType != null ? actionType : ActionType.FORCED; this.forceTime = forceTime; + this.weight = weight; } /** @@ -61,8 +71,10 @@ public class TargetWithActionType { * @param actionType * specified for the action. * @param forceTime - * after that point in time the action is exposed as forcen in + * after that point in time the action is exposed as forced in * case the type is {@link ActionType#TIMEFORCED} + * @param weight + * the priority of an {@link Action} * @param maintenanceSchedule * is the cron expression to be used for scheduling maintenance * windows. Expression has 6 mandatory fields and 1 last optional @@ -80,9 +92,9 @@ public class TargetWithActionType { * if the parameters do not define a valid maintenance schedule. */ public TargetWithActionType(final String controllerId, final ActionType actionType, final long forceTime, - final String maintenanceSchedule, final String maintenanceWindowDuration, + final Integer weight, final String maintenanceSchedule, final String maintenanceWindowDuration, final String maintenanceWindowTimeZone) { - this(controllerId, actionType, forceTime); + this(controllerId, actionType, forceTime, weight); this.maintenanceSchedule = maintenanceSchedule; this.maintenanceWindowDuration = maintenanceWindowDuration; @@ -104,6 +116,11 @@ public class TargetWithActionType { return RepositoryModelConstants.NO_FORCE_TIME; } + public Integer getWeight() { + return weight; + + } + public String getControllerId() { return controllerId; } @@ -138,14 +155,14 @@ public class TargetWithActionType { @Override public String toString() { return "TargetWithActionType [controllerId=" + controllerId + ", actionType=" + getActionType() + ", forceTime=" - + getForceTime() + ", maintenanceSchedule=" + getMaintenanceSchedule() + ", maintenanceWindowDuration=" - + getMaintenanceWindowDuration() + ", maintenanceWindowTimeZone=" + getMaintenanceWindowTimeZone() - + "]"; + + getForceTime() + ", weight=" + getWeight() + ", maintenanceSchedule=" + getMaintenanceSchedule() + + ", maintenanceWindowDuration=" + getMaintenanceWindowDuration() + ", maintenanceWindowTimeZone=" + + getMaintenanceWindowTimeZone() + "]"; } @Override public int hashCode() { - return Objects.hash(actionType, controllerId, forceTime, maintenanceSchedule, maintenanceWindowDuration, + return Objects.hash(actionType, controllerId, forceTime, weight, maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone); } @@ -163,10 +180,9 @@ public class TargetWithActionType { } final TargetWithActionType other = (TargetWithActionType) obj; return Objects.equals(actionType, other.actionType) && Objects.equals(controllerId, other.controllerId) - && Objects.equals(forceTime, other.forceTime) + && Objects.equals(forceTime, other.forceTime) && Objects.equals(weight, other.weight) && Objects.equals(maintenanceSchedule, other.maintenanceSchedule) && Objects.equals(maintenanceWindowDuration, other.maintenanceWindowDuration) && Objects.equals(maintenanceWindowTimeZone, other.maintenanceWindowTimeZone); } - } diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/AbstractRolloutManagement.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/AbstractRolloutManagement.java index 4a10ff38b..61bdc7dff 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/AbstractRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/AbstractRolloutManagement.java @@ -20,6 +20,7 @@ import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroupsValidation; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; +import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; import org.springframework.context.ApplicationContext; import org.springframework.integration.support.locks.LockRegistry; @@ -54,12 +55,18 @@ public abstract class AbstractRolloutManagement implements RolloutManagement { protected final RolloutApprovalStrategy rolloutApprovalStrategy; + protected final TenantConfigurationManagement tenantConfigurationManagement; + + protected final SystemSecurityContext systemSecurityContext; + protected AbstractRolloutManagement(final TargetManagement targetManagement, final DeploymentManagement deploymentManagement, final RolloutGroupManagement rolloutGroupManagement, final DistributionSetManagement distributionSetManagement, final ApplicationContext context, final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry, - final RolloutApprovalStrategy rolloutApprovalStrategy) { + final RolloutApprovalStrategy rolloutApprovalStrategy, + final TenantConfigurationManagement tenantConfigurationManagement, + final SystemSecurityContext systemSecurityContext) { this.targetManagement = targetManagement; this.deploymentManagement = deploymentManagement; this.rolloutGroupManagement = rolloutGroupManagement; @@ -70,6 +77,8 @@ public abstract class AbstractRolloutManagement implements RolloutManagement { this.tenantAware = tenantAware; this.lockRegistry = lockRegistry; this.rolloutApprovalStrategy = rolloutApprovalStrategy; + this.tenantConfigurationManagement = tenantConfigurationManagement; + this.systemSecurityContext = systemSecurityContext; } protected RolloutGroupsValidation validateTargetsInGroups(final List groups, final String baseFilter, @@ -156,5 +165,4 @@ public abstract class AbstractRolloutManagement implements RolloutManagement { return new AsyncResult<>(validateTargetsInGroups( groups.stream().map(RolloutGroupCreate::build).collect(Collectors.toList()), baseFilter, totalTargets)); } - } diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractRolloutUpdateCreate.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractRolloutUpdateCreate.java index 11f28ba77..f43e374b6 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractRolloutUpdateCreate.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractRolloutUpdateCreate.java @@ -10,8 +10,13 @@ package org.eclipse.hawkbit.repository.builder; import java.util.Optional; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + import org.eclipse.hawkbit.repository.ValidString; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.DistributionSet; import org.springframework.util.StringUtils; /** @@ -30,26 +35,77 @@ public abstract class AbstractRolloutUpdateCreate extends AbstractNamedEntity protected Long forcedTime; protected Long startAt; + @Min(Action.WEIGHT_MIN) + @Max(Action.WEIGHT_MAX) + protected Integer weight; + + /** + * {@link DistributionSet} of rollout + * + * @param set + * ID of the set + * @return this builder + */ public T set(final long set) { this.set = set; return (T) this; } + /** + * Filter of the rollout + * + * @param targetFilterQuery + * query + * @return this builder + */ public T targetFilterQuery(final String targetFilterQuery) { this.targetFilterQuery = StringUtils.trimWhitespace(targetFilterQuery); return (T) this; } + /** + * {@link ActionType} used for {@link Action}s + * + * @param actionType + * type + * @return this builder + */ public T actionType(final ActionType actionType) { this.actionType = actionType; return (T) this; } + /** + * forcedTime used for {@link Action}s + * + * @param forcedTime + * time + * @return this builder + */ public T forcedTime(final Long forcedTime) { this.forcedTime = forcedTime; return (T) this; } + /** + * weight used for {@link Action}s + * + * @param weight + * weight + * @return this builder + */ + public T weight(final Integer weight) { + this.weight = weight; + return (T) this; + } + + /** + * Set start of the Rollout + * + * @param startAt + * start time point + * @return this builder + */ public T startAt(final Long startAt) { this.startAt = startAt; return (T) this; @@ -67,6 +123,10 @@ public abstract class AbstractRolloutUpdateCreate extends AbstractNamedEntity return Optional.ofNullable(forcedTime); } + public Optional getWeight() { + return Optional.ofNullable(weight); + } + public Optional getStartAt() { return Optional.ofNullable(startAt); } diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractTargetFilterQueryUpdateCreate.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractTargetFilterQueryUpdateCreate.java index c7693e464..1044f353e 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractTargetFilterQueryUpdateCreate.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractTargetFilterQueryUpdateCreate.java @@ -10,8 +10,13 @@ package org.eclipse.hawkbit.repository.builder; import java.util.Optional; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + import org.eclipse.hawkbit.repository.ValidString; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.springframework.util.StringUtils; /** @@ -31,6 +36,17 @@ public abstract class AbstractTargetFilterQueryUpdateCreate extends AbstractB protected ActionType actionType; + @Min(Action.WEIGHT_MIN) + @Max(Action.WEIGHT_MAX) + protected Integer weight; + + /** + * Set DS ID of the {@link Action} created during auto assignment + * + * @param distributionSetId + * of the {@link TargetFilterQuery} + * @return this builder + */ public T autoAssignDistributionSet(final Long distributionSetId) { this.distributionSetId = distributionSetId; return (T) this; @@ -40,15 +56,46 @@ public abstract class AbstractTargetFilterQueryUpdateCreate extends AbstractB return Optional.ofNullable(distributionSetId); } + /** + * Set {@link ActionType} of the {@link Action} created during auto + * assignment + * + * @param actionType + * of the {@link TargetFilterQuery} + * @return this builder + */ public T autoAssignActionType(final ActionType actionType) { this.actionType = actionType; return (T) this; } + /** + * Set weight of the {@link Action} created during auto assignment + * + * @param weight + * of the {@link TargetFilterQuery} + * @return this builder + */ + public T autoAssignWeight(final Integer weight) { + this.weight = weight; + return (T) this; + } + + public Optional getAutoAssignWeight() { + return Optional.ofNullable(weight); + } + public Optional getAutoAssignActionType() { return Optional.ofNullable(actionType); } + /** + * Set name of the filter + * + * @param name + * of the {@link TargetFilterQuery} + * @return this builder + */ public T name(final String name) { this.name = StringUtils.trimWhitespace(name); return (T) this; @@ -58,6 +105,13 @@ public abstract class AbstractTargetFilterQueryUpdateCreate extends AbstractB return Optional.ofNullable(name); } + /** + * Set query used by the filter + * + * @param query + * of the {@link TargetFilterQuery} + * @return this builder + */ public T query(final String query) { this.query = StringUtils.trimWhitespace(query); return (T) this; diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/GenericRolloutUpdate.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/GenericRolloutUpdate.java index a7ae902a6..2b2678175 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/GenericRolloutUpdate.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/GenericRolloutUpdate.java @@ -16,5 +16,4 @@ public class GenericRolloutUpdate extends AbstractRolloutUpdateCreate, List findByTargetAndActiveOrderByIdAsc(JpaTarget target, boolean active); /** - * Retrieves the oldest {@link Action} that is active and referring to the - * given {@link Target}. - * - * @param sort - * order + * 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 * the target to find assigned actions - * @param active - * the action active flag - * - * @return the found {@link Action} + * @return the found {@link Action}s */ @EntityGraph(value = "Action.ds", type = EntityGraphType.LOAD) - Optional findFirstByTargetControllerIdAndActive(Sort sort, String controllerId, boolean active); + Page findByTargetControllerIdAndActiveIsTrueAndWeightIsNotNullOrderByWeightDescIdAsc(Pageable pageable, + String controllerId); + + /** + * 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 + * the target to find assigned actions + * @return the found {@link Action}s + */ + @EntityGraph(value = "Action.ds", type = EntityGraphType.LOAD) + Page findByTargetControllerIdAndActiveIsTrueAndWeightIsNullOrderByIdAsc(Pageable pageable, + String controllerId); /** * Checks if an active action exists for given diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaActionManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaActionManagement.java new file mode 100644 index 000000000..67ed64264 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaActionManagement.java @@ -0,0 +1,57 @@ +/** + * 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.jpa; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.repository.RepositoryProperties; +import org.eclipse.hawkbit.repository.model.Action; +import org.springframework.data.domain.PageRequest; + +/** + * Implements utility methods for managing {@link Action}s + */ +public class JpaActionManagement { + + protected final ActionRepository actionRepository; + protected final RepositoryProperties repositoryProperties; + + protected JpaActionManagement(final ActionRepository actionRepository, + final RepositoryProperties repositoryProperties) { + this.actionRepository = actionRepository; + this.repositoryProperties = repositoryProperties; + } + + protected List findActiveActionsWithHighestWeightConsideringDefault(final String controllerId, + final int maxActionCount) { + if (!actionRepository.activeActionExistsForControllerId(controllerId)) { + return Collections.emptyList(); + } + final List actions = new ArrayList<>(); + final PageRequest pageable = PageRequest.of(0, maxActionCount); + actions.addAll(actionRepository + .findByTargetControllerIdAndActiveIsTrueAndWeightIsNotNullOrderByWeightDescIdAsc(pageable, controllerId) + .getContent()); + actions.addAll(actionRepository + .findByTargetControllerIdAndActiveIsTrueAndWeightIsNullOrderByIdAsc(pageable, controllerId) + .getContent()); + final Comparator actionImportance = Comparator.comparingInt(this::getWeightConsideringDefault) + .reversed().thenComparing(Action::getId); + return actions.stream().sorted(actionImportance).limit(maxActionCount).collect(Collectors.toList()); + } + + protected int getWeightConsideringDefault(final Action action) { + return action.getWeight().orElse(repositoryProperties.getActionWeightIfAbsent()); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java index fb0eab30d..3ab73a0b5 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java @@ -110,7 +110,7 @@ import com.google.common.collect.Sets; */ @Transactional(readOnly = true) @Validated -public class JpaControllerManagement implements ControllerManagement { +public class JpaControllerManagement extends JpaActionManagement implements ControllerManagement { private static final Logger LOG = LoggerFactory.getLogger(JpaControllerManagement.class); private final BlockingDeque queue; @@ -118,9 +118,6 @@ public class JpaControllerManagement implements ControllerManagement { @Autowired private EntityManager entityManager; - @Autowired - private ActionRepository actionRepository; - @Autowired private TargetRepository targetRepository; @@ -157,10 +154,9 @@ public class JpaControllerManagement implements ControllerManagement { @Autowired private TenantAware tenantAware; - private final RepositoryProperties repositoryProperties; - JpaControllerManagement(final ScheduledExecutorService executorService, - final RepositoryProperties repositoryProperties) { + final RepositoryProperties repositoryProperties, final ActionRepository actionRepository) { + super(actionRepository, repositoryProperties); if (!repositoryProperties.isEagerPollPersistence()) { executorService.scheduleWithFixedDelay(this::flushUpdateQueue, @@ -171,8 +167,6 @@ public class JpaControllerManagement implements ControllerManagement { } else { queue = null; } - - this.repositoryProperties = repositoryProperties; } @Override @@ -346,23 +340,19 @@ public class JpaControllerManagement implements ControllerManagement { } @Override - public Optional findOldestActiveActionByTarget(final String controllerId) { - if (!actionRepository.activeActionExistsForControllerId(controllerId)) { - return Optional.empty(); - } - - // used in favorite to findFirstByTargetAndActiveOrderByIdAsc due to - // DATAJPA-841 issue. - return actionRepository.findFirstByTargetControllerIdAndActive(new Sort(Direction.ASC, "id"), controllerId, - true); + public Optional findActiveActionWithHighestWeight(final String controllerId) { + return findActiveActionsWithHighestWeight(controllerId, 1).stream().findFirst(); } @Override - public Page findActiveActionsByTarget(final Pageable pageable, final String controllerId) { - if (!actionRepository.activeActionExistsForControllerId(controllerId)) { - return Page.empty(); - } - return actionRepository.findByActiveAndTarget(pageable, controllerId, true); + public List findActiveActionsWithHighestWeight(final String controllerId, + final int maxActionCount) { + return findActiveActionsWithHighestWeightConsideringDefault(controllerId, maxActionCount); + } + + @Override + public int getWeightConsideringDefault(final Action action) { + return super.getWeightConsideringDefault(action); } @Override @@ -376,7 +366,7 @@ public class JpaControllerManagement implements ControllerManagement { } @Override - public void deleteExistingTarget(@NotEmpty String controllerId) { + public void deleteExistingTarget(@NotEmpty final String controllerId) { final Target target = targetRepository.findByControllerId(controllerId) .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); targetRepository.deleteById(target.getId()); 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 107921e57..06bcf995c 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 @@ -8,7 +8,6 @@ */ package org.eclipse.hawkbit.repository.jpa; -import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED; import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED; import java.io.Serializable; @@ -38,6 +37,7 @@ import org.eclipse.hawkbit.repository.ActionFields; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; +import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; @@ -57,6 +57,8 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper; import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; +import org.eclipse.hawkbit.repository.jpa.utils.TenantConfigHelper; +import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; @@ -103,7 +105,7 @@ import com.google.common.collect.Lists; */ @Transactional(readOnly = true) @Validated -public class JpaDeploymentManagement implements DeploymentManagement { +public class JpaDeploymentManagement extends JpaActionManagement implements DeploymentManagement { private static final Logger LOG = LoggerFactory.getLogger(JpaDeploymentManagement.class); @@ -124,7 +126,6 @@ public class JpaDeploymentManagement implements DeploymentManagement { } private final EntityManager entityManager; - private final ActionRepository actionRepository; private final DistributionSetRepository distributionSetRepository; private final TargetRepository targetRepository; private final ActionStatusRepository actionStatusRepository; @@ -146,9 +147,10 @@ public class JpaDeploymentManagement implements DeploymentManagement { 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 Database database) { + final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware, final Database database, + final RepositoryProperties repositoryProperties) { + super(actionRepository, repositoryProperties); this.entityManager = entityManager; - this.actionRepository = actionRepository; this.distributionSetRepository = distributionSetRepository; this.targetRepository = targetRepository; this.actionStatusRepository = actionStatusRepository; @@ -194,6 +196,8 @@ public class JpaDeploymentManagement implements DeploymentManagement { @Transactional(isolation = Isolation.READ_COMMITTED) public List assignDistributionSets( final List deploymentRequests, final String actionMessage) { + WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement) + .validate(deploymentRequests); return assignDistributionSets(deploymentRequests, actionMessage, onlineDsAssignmentStrategy); } @@ -275,8 +279,8 @@ public class JpaDeploymentManagement implements DeploymentManagement { final AbstractDsAssignmentStrategy assignmentStrategy) { final JpaDistributionSet distributionSetEntity = getAndValidateDsById(dsID); - final List targetIds = targetsWithActionType.stream().map(TargetWithActionType::getControllerId).distinct() - .collect(Collectors.toList()); + final List targetIds = targetsWithActionType.stream().map(TargetWithActionType::getControllerId) + .distinct().collect(Collectors.toList()); final List targetEntities = assignmentStrategy.findTargetsForAssignment(targetIds, distributionSetEntity.getId()); @@ -285,8 +289,8 @@ public class JpaDeploymentManagement implements DeploymentManagement { return allTargetsAlreadyAssignedResult(distributionSetEntity, targetsWithActionType.size()); } - final List assignedActions = doAssignDistributionSetToTargets(targetsWithActionType, - actionMessage, assignmentStrategy, distributionSetEntity, targetEntities); + final List assignedActions = doAssignDistributionSetToTargets(targetsWithActionType, actionMessage, + assignmentStrategy, distributionSetEntity, targetEntities); return buildAssignmentResult(distributionSetEntity, assignedActions, targetsWithActionType.size()); } @@ -659,6 +663,16 @@ public class JpaDeploymentManagement implements DeploymentManagement { return actionRepository.findByActiveAndTarget(pageable, controllerId, false); } + @Override + public List findActiveActionsWithHighestWeight(final String controllerId, final int maxActionCount) { + return findActiveActionsWithHighestWeightConsideringDefault(controllerId, maxActionCount); + } + + @Override + public int getWeightConsideringDefault(final Action action) { + return super.getWeightConsideringDefault(action); + } + @Override public long countActionsByTarget(final String controllerId) { throwExceptionIfTargetDoesNotExist(controllerId); @@ -823,7 +837,8 @@ public class JpaDeploymentManagement implements DeploymentManagement { } private boolean isMultiAssignmentsEnabled() { - return getConfigValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class); + return TenantConfigHelper.usingContext(systemSecurityContext, tenantConfigurationManagement) + .isMultiAssignmentsEnabled(); } private T getConfigValue(final String key, final Class valueType) { 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 342fa64ff..26a31d931 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 @@ -34,6 +34,7 @@ import org.eclipse.hawkbit.repository.RolloutHelper; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.RolloutStatusCache; import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.builder.GenericRolloutUpdate; import org.eclipse.hawkbit.repository.builder.RolloutCreate; import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; @@ -43,6 +44,8 @@ import org.eclipse.hawkbit.repository.event.remote.entity.RolloutGroupCreatedEve import org.eclipse.hawkbit.repository.event.remote.entity.RolloutUpdatedEvent; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; +import org.eclipse.hawkbit.repository.exception.NoWeightProvidedInMultiAssignmentModeException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; @@ -57,6 +60,8 @@ import org.eclipse.hawkbit.repository.jpa.specifications.RolloutSpecification; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper; import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; +import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper; +import org.eclipse.hawkbit.repository.jpa.utils.TenantConfigHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; @@ -74,6 +79,7 @@ import org.eclipse.hawkbit.repository.model.TotalTargetCountActionStatus; import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus; import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; +import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -165,9 +171,12 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { final DistributionSetManagement distributionSetManagement, final ApplicationContext context, final EventPublisherHolder eventPublisherHolder, final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry, - final Database database, final RolloutApprovalStrategy rolloutApprovalStrategy) { + final Database database, final RolloutApprovalStrategy rolloutApprovalStrategy, + final TenantConfigurationManagement tenantConfigurationManagement, + final SystemSecurityContext systemSecurityContext) { super(targetManagement, deploymentManagement, rolloutGroupManagement, distributionSetManagement, context, - virtualPropertyReplacer, txManager, tenantAware, lockRegistry, rolloutApprovalStrategy); + virtualPropertyReplacer, txManager, tenantAware, lockRegistry, rolloutApprovalStrategy, + tenantConfigurationManagement, systemSecurityContext); this.eventPublisherHolder = eventPublisherHolder; this.database = database; } @@ -226,7 +235,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { } private JpaRollout createRollout(final JpaRollout rollout) { - + WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(rollout); final Long totalTargets = targetManagement.countByRsql(rollout.getTargetFilterQuery()); if (totalTargets == 0) { throw new ValidationException("Rollout does not match any existing targets"); @@ -618,6 +627,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { action.setStatus(Status.SCHEDULED); action.setRollout(rollout); action.setRolloutGroup(rolloutGroup); + rollout.getWeight().ifPresent(action::setWeight); actionRepository.save(action); }); } @@ -1008,6 +1018,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { update.getDescription().ifPresent(rollout::setDescription); update.getActionType().ifPresent(rollout::setActionType); update.getForcedTime().ifPresent(rollout::setForcedTime); + update.getWeight().ifPresent(rollout::setWeight); update.getStartAt().ifPresent(rollout::setStartAt); update.getSet().ifPresent(setId -> { final DistributionSet set = distributionSetManagement.get(setId) @@ -1136,5 +1147,4 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { QuotaHelper.assertAssignmentQuota(target.getId(), requested, quota, Action.class, Target.class, actionRepository::countByTargetId); } - } 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 c3b767077..b05e5f221 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 @@ -18,6 +18,8 @@ import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.TargetFields; import org.eclipse.hawkbit.repository.TargetFilterQueryFields; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.builder.AutoAssignDistributionSetUpdate; import org.eclipse.hawkbit.repository.builder.GenericTargetFilterQueryUpdate; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryCreate; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryUpdate; @@ -32,11 +34,14 @@ import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; import org.eclipse.hawkbit.repository.jpa.specifications.TargetFilterQuerySpecification; import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; +import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper; +import org.eclipse.hawkbit.repository.jpa.utils.TenantConfigHelper; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; +import org.eclipse.hawkbit.security.SystemSecurityContext; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -67,19 +72,24 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme private final DistributionSetManagement distributionSetManagement; private final QuotaManagement quotaManagement; + private final TenantConfigurationManagement tenantConfigurationManagement; + private final SystemSecurityContext systemSecurityContext; private final Database database; JpaTargetFilterQueryManagement(final TargetFilterQueryRepository targetFilterQueryRepository, final TargetRepository targetRepository, final VirtualPropertyReplacer virtualPropertyReplacer, final DistributionSetManagement distributionSetManagement, final QuotaManagement quotaManagement, - final Database database) { + final Database database, final TenantConfigurationManagement tenantConfigurationManagement, + final SystemSecurityContext systemSecurityContext) { this.targetFilterQueryRepository = targetFilterQueryRepository; this.targetRepository = targetRepository; this.virtualPropertyReplacer = virtualPropertyReplacer; this.distributionSetManagement = distributionSetManagement; this.quotaManagement = quotaManagement; this.database = database; + this.tenantConfigurationManagement = tenantConfigurationManagement; + this.systemSecurityContext = systemSecurityContext; } @Override @@ -92,6 +102,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme // enforce the 'max targets per auto assign' quota right here even if // the result of the filter query can vary over time if (create.getAutoAssignDistributionSetId().isPresent()) { + WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(create); create.getQuery().ifPresent(this::assertMaxTargetsQuota); } @@ -222,30 +233,26 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme @Override @Transactional - public TargetFilterQuery updateAutoAssignDSWithActionType(final long queryId, final Long dsId, - final ActionType actionType) { - final JpaTargetFilterQuery targetFilterQuery = findTargetFilterQueryOrThrowExceptionIfNotFound(queryId); - - if (dsId == null) { + public TargetFilterQuery updateAutoAssignDS(final AutoAssignDistributionSetUpdate update) { + final JpaTargetFilterQuery targetFilterQuery = findTargetFilterQueryOrThrowExceptionIfNotFound( + update.getTargetFilterId()); + if (update.getDsId() == null) { targetFilterQuery.setAutoAssignDistributionSet(null); targetFilterQuery.setAutoAssignActionType(null); + targetFilterQuery.setAutoAssignWeight(null); } else { + WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(update); // we cannot be sure that the quota was enforced at creation time // because the Target Filter Query REST API does not allow to // specify an // auto-assign distribution set when creating a target filter query assertMaxTargetsQuota(targetFilterQuery.getQuery()); - - final JpaDistributionSet distributionSetToAutoAssign = findDistributionSetAndThrowExceptionIfNotFound(dsId); - // must be completed and not soft deleted - verifyDistributionSetAndThrowExceptionIfNotValid(distributionSetToAutoAssign); - - targetFilterQuery.setAutoAssignDistributionSet(distributionSetToAutoAssign); - // the action type is set to FORCED per default (when not explicitly - // specified) - targetFilterQuery.setAutoAssignActionType(sanitizeAutoAssignActionType(actionType)); + final JpaDistributionSet ds = findDistributionSetAndThrowExceptionIfNotFound(update.getDsId()); + verifyDistributionSetAndThrowExceptionIfNotValid(ds); + targetFilterQuery.setAutoAssignDistributionSet(ds); + targetFilterQuery.setAutoAssignActionType(sanitizeAutoAssignActionType(update.getActionType())); + targetFilterQuery.setAutoAssignWeight(update.getWeight()); } - return targetFilterQueryRepository.save(targetFilterQuery); } @@ -288,5 +295,4 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme targetRepository.count(RSQLUtility.parse(query, TargetFields.class, virtualPropertyReplacer, database)), quotaManagement.getMaxTargetsPerAutoAssignment(), Target.class, TargetFilterQuery.class); } - } 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 1f38331a9..7c072c713 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 @@ -541,9 +541,11 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final TargetFilterQueryRepository targetFilterQueryRepository, final TargetRepository targetRepository, final VirtualPropertyReplacer virtualPropertyReplacer, final DistributionSetManagement distributionSetManagement, final QuotaManagement quotaManagement, - final JpaProperties properties) { + final JpaProperties properties, final TenantConfigurationManagement tenantConfigurationManagement, + final SystemSecurityContext systemSecurityContext) { return new JpaTargetFilterQueryManagement(targetFilterQueryRepository, targetRepository, - virtualPropertyReplacer, distributionSetManagement, quotaManagement, properties.getDatabase()); + virtualPropertyReplacer, distributionSetManagement, quotaManagement, properties.getDatabase(), + tenantConfigurationManagement, systemSecurityContext); } /** @@ -620,10 +622,13 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final DistributionSetManagement distributionSetManagement, final ApplicationContext context, final EventPublisherHolder eventPublisherHolder, final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry, - final JpaProperties properties, final RolloutApprovalStrategy rolloutApprovalStrategy) { + final JpaProperties properties, final RolloutApprovalStrategy rolloutApprovalStrategy, + final TenantConfigurationManagement tenantConfigurationManagement, + final SystemSecurityContext systemSecurityContext) { return new JpaRolloutManagement(targetManagement, deploymentManagement, rolloutGroupManagement, distributionSetManagement, context, eventPublisherHolder, virtualPropertyReplacer, txManager, - tenantAware, lockRegistry, properties.getDatabase(), rolloutApprovalStrategy); + tenantAware, lockRegistry, properties.getDatabase(), rolloutApprovalStrategy, + tenantConfigurationManagement, systemSecurityContext); } /** @@ -671,11 +676,11 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final PlatformTransactionManager txManager, final TenantConfigurationManagement tenantConfigurationManagement, final QuotaManagement quotaManagement, final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware, - final JpaProperties properties) { + 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()); + properties.getDatabase(), repositoryProperties); } /** @@ -686,8 +691,8 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { @Bean @ConditionalOnMissingBean ControllerManagement controllerManagement(final ScheduledExecutorService executorService, - final RepositoryProperties repositoryProperties) { - return new JpaControllerManagement(executorService, repositoryProperties); + final RepositoryProperties repositoryProperties, final ActionRepository actionRepository) { + return new JpaControllerManagement(executorService, repositoryProperties, actionRepository); } @Bean diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java index 8364dc001..49bfd7b9f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java @@ -97,6 +97,8 @@ public class AutoAssignChecker { final Page filterQueries = targetFilterQueryManagement.findWithAutoAssignDS(pageRequest); + // we should ensure that the filter queries are executed + // in the order of weights for (final TargetFilterQuery filterQuery : filterQueries) { checkByTargetFilterQueryAndAssignDS(filterQuery); } @@ -143,7 +145,8 @@ public class AutoAssignChecker { return DeploymentHelper.runInNewTransaction(transactionManager, "autoAssignDSToTargets", Isolation.READ_COMMITTED.value(), status -> { final List deploymentRequests = createAssignmentRequests( - targetFilterQuery.getQuery(), dsId, targetFilterQuery.getAutoAssignActionType(), PAGE_SIZE); + targetFilterQuery.getQuery(), dsId, targetFilterQuery.getAutoAssignActionType(), + targetFilterQuery.getAutoAssignWeight().orElse(null), PAGE_SIZE); final int count = deploymentRequests.size(); if (count > 0) { deploymentManagement.assignDistributionSets(deploymentRequests, actionMessage); @@ -168,17 +171,15 @@ public class AutoAssignChecker { * @return list of targets with action type */ private List createAssignmentRequests(final String targetFilterQuery, final Long dsId, - final ActionType type, final int count) { + final ActionType type, final Integer weight, final int count) { final Page targets = targetManagement.findByTargetFilterQueryAndNonDS(PageRequest.of(0, count), dsId, targetFilterQuery); // the action type is set to FORCED per default (when not explicitly // specified) final ActionType autoAssignActionType = type == null ? ActionType.FORCED : type; - return targets.getContent().stream() - .map(t -> DeploymentManagement.deploymentRequest(t.getControllerId(), dsId) - .setActionType(autoAssignActionType).build()) - .collect(Collectors.toList()); + return targets.getContent().stream().map(t -> DeploymentManagement.deploymentRequest(t.getControllerId(), dsId) + .setActionType(autoAssignActionType).setWeight(weight).build()).collect(Collectors.toList()); } } 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 f9e791b4f..9011b5026 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 @@ -31,6 +31,7 @@ public class JpaRolloutCreate extends AbstractRolloutUpdateCreate rollout.setDistributionSet(findDistributionSetAndThrowExceptionIfNotFound(set)); rollout.setTargetFilterQuery(targetFilterQuery); rollout.setStartAt(startAt); + rollout.setWeight(weight); if (actionType != null) { rollout.setActionType(actionType); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetFilterQueryBuilder.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetFilterQueryBuilder.java index b6a91c8cc..24f4e4867 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetFilterQueryBuilder.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetFilterQueryBuilder.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.repository.jpa.builder; import org.eclipse.hawkbit.repository.DistributionSetManagement; +import org.eclipse.hawkbit.repository.builder.AutoAssignDistributionSetUpdate; import org.eclipse.hawkbit.repository.builder.GenericTargetFilterQueryUpdate; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryBuilder; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryCreate; @@ -36,4 +37,9 @@ public class JpaTargetFilterQueryBuilder implements TargetFilterQueryBuilder { return new JpaTargetFilterQueryCreate(distributionSetManagement); } + @Override + public AutoAssignDistributionSetUpdate updateAutoAssign(final long id) { + return new AutoAssignDistributionSetUpdate(id); + } + } 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 163aa6efb..f73b498ff 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 @@ -36,7 +36,8 @@ public class JpaTargetFilterQueryCreate extends AbstractTargetFilterQueryUpdateC return new JpaTargetFilterQuery(name, query, getAutoAssignDistributionSetId().map(this::findDistributionSetAndThrowExceptionIfNotFound).orElse(null), - getAutoAssignActionType().filter(JpaTargetFilterQueryCreate::isAutoAssignActionTypeValid).orElse(null)); + getAutoAssignActionType().filter(JpaTargetFilterQueryCreate::isAutoAssignActionTypeValid).orElse(null), + weight); } private DistributionSet findDistributionSetAndThrowExceptionIfNotFound(final Long setId) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java index 243d07afd..3b86ba46a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java @@ -28,6 +28,8 @@ import javax.persistence.NamedEntityGraphs; import javax.persistence.NamedSubgraph; import javax.persistence.OneToMany; import javax.persistence.Table; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; @@ -89,6 +91,11 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio @Column(name = "forced_time") private long forcedTime; + @Column(name = "weight") + @Min(Action.WEIGHT_MIN) + @Max(Action.WEIGHT_MAX) + private Integer weight; + @Column(name = "status", nullable = false) @ObjectTypeConverter(name = "status", objectType = Action.Status.class, dataType = Integer.class, conversionValues = { @ConversionValue(objectValue = "FINISHED", dataValue = "0"), @@ -126,7 +133,7 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio @Column(name = "maintenance_time_zone", updatable = false, length = Action.MAINTENANCE_WINDOW_TIMEZONE_LENGTH) private String maintenanceWindowTimeZone; - + @Column(name = "external_ref", length = Action.EXTERNAL_REF_MAX_LENGTH) private String externalRef; @@ -192,6 +199,15 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio this.forcedTime = forcedTime; } + @Override + public Optional getWeight() { + return Optional.ofNullable(weight); + } + + public void setWeight(final Integer weight) { + this.weight = weight; + } + @Override public RolloutGroup getRolloutGroup() { return rolloutGroup; @@ -213,8 +229,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio @Override public String toString() { return "JpaAction [distributionSet=" + distributionSet.getId() + ", version=" + getOptLockRevision() + ", id=" - + getId() + ", actionType=" + getActionType() + ", isActive=" + isActive() + ", createdAt=" - + getCreatedAt() + ", lastModifiedAt=" + getLastModifiedAt() + "]"; + + getId() + ", actionType=" + getActionType() + ", weight=" + getWeight() + ", isActive=" + isActive() + + ", createdAt=" + getCreatedAt() + ", lastModifiedAt=" + getLastModifiedAt() + "]"; } @Override @@ -337,7 +353,7 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio } @Override - public void setExternalRef(String externalRef) { + public void setExternalRef(final String externalRef) { this.externalRef = externalRef; } 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 535215a4d..ecf156e16 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 @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.repository.jpa.model; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import javax.persistence.Column; @@ -23,6 +24,8 @@ import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Transient; import javax.persistence.UniqueConstraint; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @@ -90,7 +93,7 @@ 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") }) @Convert("rolloutstatus") @NotNull private RolloutStatus status = RolloutStatus.CREATING; @@ -131,6 +134,11 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event @Size(max = Rollout.APPROVAL_REMARK_MAX_SIZE) private String approvalRemark; + @Column(name = "weight") + @Min(Action.WEIGHT_MIN) + @Max(Action.WEIGHT_MAX) + private Integer weight; + @Transient private transient TotalTargetCountStatus totalTargetCountStatus; @@ -204,6 +212,15 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event this.forcedTime = forcedTime; } + @Override + public Optional getWeight() { + return Optional.ofNullable(weight); + } + + public void setWeight(final Integer weight) { + this.weight = weight; + } + @Override public long getTotalTargets() { return totalTargets; @@ -299,5 +316,4 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event public void setApprovalRemark(final String approvalRemark) { this.approvalRemark = approvalRemark; } - } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetFilterQuery.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetFilterQuery.java index 2a7737882..af945a327 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetFilterQuery.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetFilterQuery.java @@ -8,6 +8,8 @@ */ package org.eclipse.hawkbit.repository.jpa.model; +import java.util.Optional; + import javax.persistence.Column; import javax.persistence.ConstraintMode; import javax.persistence.Entity; @@ -66,11 +68,15 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity @ObjectTypeConverter(name = "autoAssignActionType", objectType = Action.ActionType.class, dataType = Integer.class, conversionValues = { @ConversionValue(objectValue = "FORCED", dataValue = "0"), @ConversionValue(objectValue = "SOFT", dataValue = "1"), - // Conversion for 'TIMEFORCED' is disabled because it is not permitted in autoAssignment - @ConversionValue(objectValue = "DOWNLOAD_ONLY", dataValue = "3")}) + // Conversion for 'TIMEFORCED' is disabled because it is not + // permitted in autoAssignment + @ConversionValue(objectValue = "DOWNLOAD_ONLY", dataValue = "3") }) @Convert("autoAssignActionType") private ActionType autoAssignActionType; + @Column(name = "auto_assign_weight", nullable = true) + private Integer autoAssignWeight; + public JpaTargetFilterQuery() { // Default constructor for JPA. } @@ -86,13 +92,16 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity * of the {@link TargetFilterQuery}. * @param autoAssignActionType * of the {@link TargetFilterQuery}. + * @param autoAssignWeight + * of the {@link TargetFilterQuery}. */ public JpaTargetFilterQuery(final String name, final String query, final DistributionSet autoAssignDistributionSet, - final ActionType autoAssignActionType) { + final ActionType autoAssignActionType, final Integer autoAssignWeight) { this.name = name; this.query = query; this.autoAssignDistributionSet = (JpaDistributionSet) autoAssignDistributionSet; this.autoAssignActionType = autoAssignActionType; + this.autoAssignWeight = autoAssignWeight; } @Override @@ -131,6 +140,15 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity this.autoAssignActionType = actionType; } + @Override + public Optional getAutoAssignWeight() { + return Optional.ofNullable(autoAssignWeight); + } + + public void setAutoAssignWeight(final Integer weight) { + this.autoAssignWeight = weight; + } + @Override public void fireCreateEvent(final DescriptorEvent descriptorEvent) { EventPublisherHolder.getInstance().getEventPublisher().publishEvent( diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/TenantConfigHelper.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/TenantConfigHelper.java new file mode 100644 index 000000000..d79a38861 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/TenantConfigHelper.java @@ -0,0 +1,53 @@ +/** + * 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.jpa.utils; + +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED; + +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.security.SystemSecurityContext; + +/** + * A collection of static helper methods for the tenant configuration + */ +public final class TenantConfigHelper { + + private final TenantConfigurationManagement tenantConfigurationManagement; + private final SystemSecurityContext systemSecurityContext; + + private TenantConfigHelper(final SystemSecurityContext systemSecurityContext, + final TenantConfigurationManagement tenantConfigurationManagement) { + this.systemSecurityContext = systemSecurityContext; + this.tenantConfigurationManagement = tenantConfigurationManagement; + } + + /** + * Setting the context of the tenant. + * + * @param systemSecurityContext + * Security context used to get the tenant and for execution + * @param tenantConfigurationManagement + * to get the value from + * @return is active + */ + public static TenantConfigHelper usingContext(final SystemSecurityContext systemSecurityContext, + final TenantConfigurationManagement tenantConfigurationManagement) { + return new TenantConfigHelper(systemSecurityContext, tenantConfigurationManagement); + } + + /** + * Is multi-assignments enabled for the current tenant + * + * @return is active + */ + public boolean isMultiAssignmentsEnabled() { + return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement + .getConfigurationValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class).getValue()); + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/WeightValidationHelper.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/WeightValidationHelper.java new file mode 100644 index 000000000..3e333b391 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/WeightValidationHelper.java @@ -0,0 +1,132 @@ +/** + * 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.jpa.utils; + +import java.util.List; + +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.builder.AutoAssignDistributionSetUpdate; +import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; +import org.eclipse.hawkbit.repository.exception.NoWeightProvidedInMultiAssignmentModeException; +import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryCreate; +import org.eclipse.hawkbit.repository.model.DeploymentRequest; +import org.eclipse.hawkbit.repository.model.Rollout; +import org.eclipse.hawkbit.security.SystemSecurityContext; + +/** + * Utility class to handle weight validation in Rollout, Auto Assignments, and + * Online Assignment. + */ +public final class WeightValidationHelper { + + private final TenantConfigurationManagement tenantConfigurationManagement; + private final SystemSecurityContext systemSecurityContext; + + private WeightValidationHelper(final SystemSecurityContext systemSecurityContext, + final TenantConfigurationManagement tenantConfigurationManagement) { + this.systemSecurityContext = systemSecurityContext; + this.tenantConfigurationManagement = tenantConfigurationManagement; + } + + /** + * Setting the context of the tenant + * + * @param systemSecurityContext + * security context used to get the tenant and for execution + * @param tenantConfigurationManagement + * to get the value from + */ + public static WeightValidationHelper usingContext(final SystemSecurityContext systemSecurityContext, + final TenantConfigurationManagement tenantConfigurationManagement) { + return new WeightValidationHelper(systemSecurityContext, tenantConfigurationManagement); + } + + /** + * Validating weights associated with all the {@link DeploymentRequest}s + * + * @param deploymentRequests + * the {@linkplain List} of {@link DeploymentRequest}s + */ + public void validate(final List deploymentRequests) { + final long assignmentsWithWeight = deploymentRequests.stream() + .filter(request -> request.getTargetWithActionType().getWeight() != null).count(); + final boolean containsAssignmentWithWeight = assignmentsWithWeight > 0; + final boolean containsAssignmentWithoutWeight = assignmentsWithWeight < deploymentRequests.size(); + + validateWeight(containsAssignmentWithWeight, containsAssignmentWithoutWeight); + } + + /** + * Validating weight associated with the {@link Rollout} + * + * @param rollout + * the {@linkplain Rollout} + */ + public void validate(final Rollout rollout) { + validateWeight(rollout.getWeight().orElse(null)); + } + + /** + * Validating weight associated with the target filter query + * + * @param targetFilterQueryCreate + * the target filter query + */ + public void validate(final JpaTargetFilterQueryCreate targetFilterQueryCreate) { + validateWeight(targetFilterQueryCreate.getAutoAssignWeight().orElse(null)); + + } + + /** + * Validating weight associated with the auto assignment + * + * @param autoAssignDistributionSetUpdate + * the auto assignment distribution set update + */ + public void validate(final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate) { + validateWeight(autoAssignDistributionSetUpdate.getWeight()); + + } + + /** + * Checks if the weight is valid + * + * @param weight + * weight tied to the rollout, auto assignment, or online + * assignment. + */ + public void validateWeight(final Integer weight) { + final boolean hasWeight = weight != null; + validateWeight(hasWeight, !hasWeight); + } + + /** + * Checks if the weight is valid with the multi-assignments being turned + * off/on. + * + * @param hasWeight + * indicator of the weight if it has numerical value + * @param hasNoWeight + * indicator of the weight if it doesn't have a numerical value + */ + public void validateWeight(final boolean hasWeight, final boolean hasNoWeight) { + // remove bypassing the weight enforcement as soon as weight can be set + // via UI + final boolean bypassWeightEnforcement = true; + final boolean multiAssignmentsEnabled = TenantConfigHelper + .usingContext(systemSecurityContext, tenantConfigurationManagement).isMultiAssignmentsEnabled(); + if (!multiAssignmentsEnabled && hasWeight) { + throw new MultiAssignmentIsNotEnabledException(); + } else if (bypassWeightEnforcement) { + return; + } else if (multiAssignmentsEnabled && hasNoWeight) { + throw new NoWeightProvidedInMultiAssignmentModeException(); + } + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_15__add_weight___DB2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_15__add_weight___DB2.sql new file mode 100644 index 000000000..9eaad20e1 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_15__add_weight___DB2.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_action ADD weight INT; +ALTER TABLE sp_rollout ADD weight INT; +ALTER TABLE sp_target_filter_query ADD auto_assign_weight INT; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_15__add_weight___H2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_15__add_weight___H2.sql new file mode 100644 index 000000000..9eaad20e1 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_15__add_weight___H2.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_action ADD weight INT; +ALTER TABLE sp_rollout ADD weight INT; +ALTER TABLE sp_target_filter_query ADD auto_assign_weight INT; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_15__add_weight___MYSQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_15__add_weight___MYSQL.sql new file mode 100644 index 000000000..9eaad20e1 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_15__add_weight___MYSQL.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_action ADD weight INT; +ALTER TABLE sp_rollout ADD weight INT; +ALTER TABLE sp_target_filter_query ADD auto_assign_weight INT; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_15__add_weight___SQL_SERVER.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_15__add_weight___SQL_SERVER.sql new file mode 100644 index 000000000..9eaad20e1 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_15__add_weight___SQL_SERVER.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_action ADD weight INT; +ALTER TABLE sp_rollout ADD weight INT; +ALTER TABLE sp_target_filter_query ADD auto_assign_weight INT; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteIdEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteIdEventTest.java index 1094d8a68..c664644f9 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteIdEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteIdEventTest.java @@ -105,5 +105,4 @@ public class RemoteIdEventTest extends AbstractRemoteEventTest { // gets added because events inherit from of java.util.EventObject assertThat(underTestCreatedEvent).isEqualToIgnoringGivenFields(event, "source"); } - } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteTenantAwareEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteTenantAwareEventTest.java index 1efb4b091..e94c3e2b7 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteTenantAwareEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteTenantAwareEventTest.java @@ -98,5 +98,4 @@ public class RemoteTenantAwareEventTest extends AbstractRemoteEventTest { assertThat(actionProperties).isEqualToComparingFieldByField(new ActionProperties(action)); assertThat(underTest.getDistributionSetId()).isEqualTo(action.getDistributionSet().getId()); } - } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java index e1c18f02e..e37d01e20 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java @@ -104,7 +104,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { assertThat(controllerManagement.getActionForDownloadByTargetAndSoftwareModule(target.getControllerId(), module.getId())).isNotPresent(); - assertThat(controllerManagement.findOldestActiveActionByTarget(NOT_EXIST_ID)).isNotPresent(); + assertThat(controllerManagement.findActiveActionWithHighestWeight(NOT_EXIST_ID)).isNotPresent(); assertThat(controllerManagement.hasTargetArtifactAssigned(target.getControllerId(), "XXX")).isFalse(); assertThat(controllerManagement.hasTargetArtifactAssigned(target.getId(), "XXX")).isFalse(); @@ -457,7 +457,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { .getContent(); assertThat(actionStatusList.get(actionStatusList.size() - 1).getStatus()).isEqualTo(expectedActionStatus); if (actionActive) { - assertThat(controllerManagement.findOldestActiveActionByTarget(controllerId).get().getId()) + assertThat(controllerManagement.findActiveActionWithHighestWeight(controllerId).get().getId()) .isEqualTo(actionId); } } @@ -501,7 +501,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Register a controller which does not exist") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 2)}) + @Expect(type = TargetPollEvent.class, count = 2) }) public void findOrRegisterTargetIfItDoesNotExist() { final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST); assertThat(target).as("target should not be null").isNotNull(); @@ -517,7 +517,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Expect(type = TargetPollEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 1) }) public void findOrRegisterTargetIfItDoesNotExistWithName() { final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, "TestName"); - final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, + final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, "ChangedTestName"); assertThat(target.getId()).as("Target should be the equals").isEqualTo(sameTarget.getId()); assertThat(target.getName()).as("Taget names should be different").isNotEqualTo(sameTarget.getName()); @@ -560,8 +560,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST)); verify(mockTargetRepository, times(TX_RT_MAX)).findOne(any()); - } - finally { + } finally { // revert ((JpaControllerManagement) controllerManagement).setTargetRepository(targetRepository); } @@ -597,22 +596,23 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Register a controller which does not exist, then update the controller twice, first time by providing a name property and second time without a new name") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 3) , - @Expect(type = TargetUpdatedEvent.class, count = 1)}) + @Expect(type = TargetPollEvent.class, count = 3), @Expect(type = TargetUpdatedEvent.class, count = 1) }) public void findOrRegisterTargetIfItDoesNotExistDoesUpdateNameOnExistingTargetProperly() { - String controllerId = "12345"; - String targetName = "UpdatedName"; + final String controllerId = "12345"; + final String targetName = "UpdatedName"; - final Target newTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, LOCALHOST); + final Target newTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, LOCALHOST); assertThat(newTarget.getName()).isEqualTo(controllerId); - - Target firstTimeUpdatedTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, LOCALHOST, targetName); + final Target firstTimeUpdatedTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, + LOCALHOST, targetName); assertThat(firstTimeUpdatedTarget.getName()).isEqualTo(targetName); - //Name should not change to default (name=targetId) if target is updated without new name provided - Target secondTimeUpdatedTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, LOCALHOST); + // Name should not change to default (name=targetId) if target is + // updated without new name provided + final Target secondTimeUpdatedTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, + LOCALHOST); assertThat(secondTimeUpdatedTarget.getName()).isEqualTo(targetName); } @@ -633,8 +633,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist("1234", LOCALHOST)); verify(mockTargetRepository, times(1)).findOne(any()); verify(mockTargetRepository, times(1)).save(any()); - } - finally { + } finally { // revert ((JpaControllerManagement) controllerManagement).setTargetRepository(targetRepository); } @@ -651,13 +650,11 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { when(mockTargetRepository.findOne(any())).thenThrow(RuntimeException.class); try { - assertThatExceptionOfType(RuntimeException.class) - .as("Expected a RuntimeException to be thrown!") + assertThatExceptionOfType(RuntimeException.class).as("Expected a RuntimeException to be thrown!") .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist("aControllerId", LOCALHOST)); verify(mockTargetRepository, times(1)).findOne(any()); - } - finally { + } finally { // revert ((JpaControllerManagement) controllerManagement).setTargetRepository(targetRepository); } @@ -1054,9 +1051,8 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { Collections.singletonMap(keyValid, valueTooLong), null)); assertThatExceptionOfType(InvalidTargetAttributeException.class) - .as("Attribute with key NULL should not be created") - .isThrownBy(() -> controllerManagement.updateControllerAttributes(controllerId, - Collections.singletonMap(keyNull, valueValid), null)); + .as("Attribute with key NULL should not be created").isThrownBy(() -> controllerManagement + .updateControllerAttributes(controllerId, Collections.singletonMap(keyNull, valueValid), null)); } @Test @@ -1298,9 +1294,9 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { assertThat(actionId).isNotNull(); assertThatExceptionOfType(AssignmentQuotaExceededException.class) - .as("No QuotaExceededException thrown for too many DOWNLOADED updateActionStatus updates") - .isThrownBy(() -> IntStream.range(0, maxMessages).forEach(i -> controllerManagement - .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED)))); + .as("No QuotaExceededException thrown for too many DOWNLOADED updateActionStatus updates").isThrownBy( + () -> IntStream.range(0, maxMessages).forEach(i -> controllerManagement.addUpdateActionStatus( + entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED)))); assertThatExceptionOfType(AssignmentQuotaExceededException.class) .as("No QuotaExceededException thrown for too many ERROR updateActionStatus updates") @@ -1326,18 +1322,18 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { assertThat(actionId).isNotNull(); assertThatExceptionOfType(AssignmentQuotaExceededException.class) - .as("No QuotaExceededException thrown for too many DOWNLOADED updateActionStatus updates") - .isThrownBy( ()-> IntStream.range(0, maxMessages).forEach(i -> controllerManagement - .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED)))); + .as("No QuotaExceededException thrown for too many DOWNLOADED updateActionStatus updates").isThrownBy( + () -> IntStream.range(0, maxMessages).forEach(i -> controllerManagement.addUpdateActionStatus( + entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED)))); assertThatExceptionOfType(AssignmentQuotaExceededException.class) .as("No QuotaExceededException thrown for too many ERROR updateActionStatus updates") - .isThrownBy(()->IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .isThrownBy(() -> IntStream.range(0, maxMessages).forEach(i -> controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.ERROR)))); assertThatExceptionOfType(AssignmentQuotaExceededException.class) .as("No QuotaExceededException thrown for too many FINISHED updateActionStatus updates") - .isThrownBy(()->IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .isThrownBy(() -> IntStream.range(0, maxMessages).forEach(i -> controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.FINISHED)))); } @@ -1472,6 +1468,39 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { assertActionStatus(actionId, controllerId, TargetUpdateStatus.IN_SYNC, actionStatus, actionStatus, false); } + @Test + @Description("Actions are exposed according to thier weight in multi assignment mode.") + public void actionsAreExposedAccordingToTheirWeight() { + final String targetId = testdataFactory.createTarget().getControllerId(); + final DistributionSet ds = testdataFactory.createDistributionSet(); + final Long actionWeightNull = assignDistributionSet(ds.getId(), targetId).getAssignedEntity().get(0).getId(); + enableMultiAssignments(); + final Long actionWeight500old = assignDistributionSet(ds.getId(), targetId, 500).getAssignedEntity().get(0) + .getId(); + final Long actionWeight500new = assignDistributionSet(ds.getId(), targetId, 500).getAssignedEntity().get(0) + .getId(); + final Long actionWeight1000 = assignDistributionSet(ds.getId(), targetId, 1000).getAssignedEntity().get(0) + .getId(); + + assertThat(controllerManagement.findActiveActionWithHighestWeight(targetId).get().getId()) + .isEqualTo(actionWeightNull); + controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionWeightNull).status(Status.FINISHED)); + assertThat(controllerManagement.findActiveActionWithHighestWeight(targetId).get().getId()) + .isEqualTo(actionWeight1000); + controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionWeight1000).status(Status.FINISHED)); + assertThat(controllerManagement.findActiveActionWithHighestWeight(targetId).get().getId()) + .isEqualTo(actionWeight500old); + controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionWeight500old).status(Status.FINISHED)); + assertThat(controllerManagement.findActiveActionWithHighestWeight(targetId).get().getId()) + .isEqualTo(actionWeight500new); + controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionWeight500new).status(Status.FINISHED)); + assertThat(controllerManagement.findActiveActionWithHighestWeight(targetId)).isEmpty(); + } + private void assertAssignedDistributionSetId(final String controllerId, final Long dsId) { final Optional target = controllerManagement.getByControllerId(controllerId); assertThat(target).isPresent(); @@ -1494,12 +1523,10 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { assertThat(actionRepository.activeActionExistsForControllerId(controllerId)).isEqualTo(false); } - @Test @Description("Delete a target on requested target deletion from client side") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 1), - @Expect(type = TargetDeletedEvent.class, count = 1)}) + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1), @Expect(type = TargetDeletedEvent.class, count = 1) }) public void deleteTargetWithValidThingId() { final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST); assertThat(target).as("target should not be null").isNotNull(); @@ -1512,7 +1539,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Delete a target with a non existing thingId") - @ExpectEvents({@Expect(type = TargetDeletedEvent.class, count = 0)}) + @ExpectEvents({ @Expect(type = TargetDeletedEvent.class, count = 0) }) public void deleteTargetWithInvalidThingId() { assertThatExceptionOfType(EntityNotFoundException.class) .as("No EntityNotFoundException thrown when deleting a non-existing target") @@ -1522,9 +1549,8 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Delete a target after it has been deleted already") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 1), - @Expect(type = TargetDeletedEvent.class, count = 1)}) + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1), @Expect(type = TargetDeletedEvent.class, count = 1) }) public void deleteTargetAfterItWasDeleted() { final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST); assertThat(target).as("target should not be null").isNotNull(); 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 4de4ea3e0..3bf0e5446 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 @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map.Entry; import java.util.stream.Collectors; +import javax.validation.ConstraintViolationException; + import org.assertj.core.api.Assertions; import org.eclipse.hawkbit.repository.ActionStatusFields; import org.eclipse.hawkbit.repository.DeploymentManagement; @@ -171,7 +173,7 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { } assertThatExceptionOfType(AssignmentQuotaExceededException.class) - .isThrownBy(() -> assignDistributionSet(ds1, Collections.singletonList(testTarget))); + .isThrownBy(() -> assignDistributionSet(ds1.getId(), testTarget.getControllerId(), 77)); } @Test @@ -563,16 +565,17 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { public void previousAssignmentsAreNotCanceledInMultiAssignMode() { enableMultiAssignments(); final List targets = testdataFactory.createTargets(10); + final List targetIds = targets.stream().map(Target::getControllerId).collect(Collectors.toList()); // First assignment final DistributionSet ds1 = testdataFactory.createDistributionSet("Multi-assign-1"); - assignDistributionSet(ds1, targets); + assignDistributionSet(ds1.getId(), targetIds, 77); assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING); // Second assignment final DistributionSet ds2 = testdataFactory.createDistributionSet("Multi-assign-2"); - assignDistributionSet(ds2, targets); + assignDistributionSet(ds2.getId(), targetIds, 45); assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, Status.RUNNING); assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING); @@ -601,7 +604,7 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { public void multiassignmentInOneRequest() { final List targets = testdataFactory.createTargets(2); final List distributionSets = testdataFactory.createDistributionSets(2); - final List deploymentRequests = createAssignmentRequests(distributionSets, targets); + final List deploymentRequests = createAssignmentRequests(distributionSets, targets, 34); enableMultiAssignments(); final List results = deploymentManagement @@ -614,7 +617,14 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { .stream().map(action -> action.getDistributionSet().getId()).collect(Collectors.toList()); assertThat(assignedDsIds).containsExactlyInAnyOrderElementsOf(dsIds); }); + } + protected List createAssignmentRequests(final Collection distributionSets, + final Collection targets, final int weight) { + final List deploymentRequests = new ArrayList<>(); + distributionSets.forEach(ds -> targets.forEach(target -> deploymentRequests.add(DeploymentManagement + .deploymentRequest(target.getControllerId(), ds.getId()).setWeight(weight).build()))); + return deploymentRequests; } @Test @@ -624,9 +634,10 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { final List distributionSets = testdataFactory.createDistributionSets(2); final DeploymentRequest targetToDS0 = DeploymentManagement - .deploymentRequest(target.getControllerId(), distributionSets.get(0).getId()).build(); + .deploymentRequest(target.getControllerId(), distributionSets.get(0).getId()).setWeight(78).build(); + final DeploymentRequest targetToDS1 = DeploymentManagement - .deploymentRequest(target.getControllerId(), distributionSets.get(1).getId()).build(); + .deploymentRequest(target.getControllerId(), distributionSets.get(1).getId()).setWeight(565).build(); Assertions.assertThatExceptionOfType(MultiAssignmentIsNotEnabledException.class) .isThrownBy(() -> deploymentManagement.assignDistributionSets(Arrays.asList(targetToDS0, targetToDS1))); @@ -638,17 +649,26 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Duplicate Assignments are removed from a request when multiassignment is disabled, otherwise not") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 3), @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = MultiActionEvent.class, count = 1) }) public void duplicateAssignmentsInRequestAreOnlyRemovedIfMultiassignmentDisabled() { - final Target target = testdataFactory.createTarget(); - final DistributionSet ds = testdataFactory.createDistributionSet(); + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); final List twoEqualAssignments = Collections.nCopies(2, - DeploymentManagement.deploymentRequest(target.getControllerId(), ds.getId()).build()); + DeploymentManagement.deploymentRequest(targetId, dsId).build()); assertThat(getResultingActionCount(deploymentManagement.assignDistributionSets(twoEqualAssignments))) .isEqualTo(1); enableMultiAssignments(); - assertThat(getResultingActionCount(deploymentManagement.assignDistributionSets(twoEqualAssignments))) + final List twoEqualAssignmentsWithWeight = Collections.nCopies(2, + DeploymentManagement.deploymentRequest(targetId, dsId).setWeight(555).build()); + + assertThat(getResultingActionCount(deploymentManagement.assignDistributionSets(twoEqualAssignmentsWithWeight))) .isEqualTo(2); } @@ -668,7 +688,7 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { final Long dsId = testdataFactory.createDistributionSet().getId(); final List deploymentRequests = Collections.nCopies(maxActions + 1, - DeploymentManagement.deploymentRequest(controllerId, dsId).build()); + DeploymentManagement.deploymentRequest(controllerId, dsId).setWeight(24).build()); enableMultiAssignments(); Assertions.assertThatExceptionOfType(AssignmentQuotaExceededException.class) @@ -676,6 +696,67 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { assertThat(actionRepository.countByTargetControllerId(controllerId)).isEqualTo(0); } + @Test + @Description("An assignment request without a weight is ok when multi assignment in enabled.") + public void weightNotRequiredInMultiAssignmentMode() { + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + + final DeploymentRequest assignWithoutWeight = DeploymentManagement.deploymentRequest(targetId, dsId).build(); + final DeploymentRequest assignWithWeight = DeploymentManagement.deploymentRequest(targetId, dsId).setWeight(567) + .build(); + + enableMultiAssignments(); + deploymentManagement.assignDistributionSets(Arrays.asList(assignWithoutWeight, assignWithWeight)); + } + + @Test + @Description("An assignment request containing a weight causes an error when multi assignment in disabled.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void weightNotAllowedWhenMultiAssignmentModeNotEnabled() { + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + + final DeploymentRequest assignWithoutWeight = DeploymentManagement.deploymentRequest(targetId, dsId) + .setWeight(456).build(); + + Assertions.assertThatExceptionOfType(MultiAssignmentIsNotEnabledException.class).isThrownBy( + () -> deploymentManagement.assignDistributionSets(Collections.singletonList(assignWithoutWeight))); + } + + @Test + @Description("Weights are validated and contained in the resulting Action.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = ActionCreatedEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = MultiActionEvent.class, count = 2) }) + public void weightValidatedAndSaved() { + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + final DeploymentRequest valideRequest1 = DeploymentManagement.deploymentRequest(targetId, dsId) + .setWeight(Action.WEIGHT_MAX).build(); + final DeploymentRequest valideRequest2 = DeploymentManagement.deploymentRequest(targetId, dsId) + .setWeight(Action.WEIGHT_MIN).build(); + final DeploymentRequest weightTooLow = DeploymentManagement.deploymentRequest(targetId, dsId) + .setWeight(Action.WEIGHT_MIN - 1).build(); + final DeploymentRequest weightTooHigh = DeploymentManagement.deploymentRequest(targetId, dsId) + .setWeight(Action.WEIGHT_MAX + 1).build(); + enableMultiAssignments(); + Assertions.assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> deploymentManagement.assignDistributionSets(Collections.singletonList(weightTooLow))); + Assertions.assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy( + () -> deploymentManagement.assignDistributionSets(Collections.singletonList(weightTooHigh))); + final Long valideActionId1 = getFirstAssignedAction( + deploymentManagement.assignDistributionSets(Collections.singletonList(valideRequest1)).get(0)).getId(); + final Long valideActionId2 = getFirstAssignedAction( + deploymentManagement.assignDistributionSets(Collections.singletonList(valideRequest2)).get(0)).getId(); + assertThat(actionRepository.getById(valideActionId1).get().getWeight()).get().isEqualTo(Action.WEIGHT_MAX); + assertThat(actionRepository.getById(valideActionId2).get().getWeight()).get().isEqualTo(Action.WEIGHT_MIN); + } + /** * test a simple deployment by calling the * {@link TargetRepository#assignDistributionSet(DistributionSet, Iterable)} @@ -1143,13 +1224,15 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Tests the computation of already assigned entities returned as a result of an assignment") - public void testAlreadyAssignedAndAssignedActionsInAssignmentResult(){ - // create target1, distributionSet, assign ds to target1 and finish update (close all actions) + public void testAlreadyAssignedAndAssignedActionsInAssignmentResult() { + // create target1, distributionSet, assign ds to target1 and finish + // update (close all actions) final Action action = prepareFinishedUpdate("target1", "ds", false); final Target target2 = testdataFactory.createTarget("target2"); final Target target3 = testdataFactory.createTarget("target3"); - // assign ds to target2, but don't finish update (actions should be still open) + // assign ds to target2, but don't finish update (actions should be + // still open) assignDistributionSet(action.getDistributionSet().getId(), target2.getControllerId()); final DistributionSetAssignmentResult assignmentResult = assignDistributionSet( @@ -1283,7 +1366,5 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { public List getUndeployedTargetIDs() { return undeployedTargetIDs; } - } - } 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 b4376001f..02eea953a 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 @@ -17,12 +17,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import javax.validation.ConstraintViolationException; import javax.validation.ValidationException; +import org.assertj.core.api.Assertions; import org.assertj.core.api.Condition; import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; import org.eclipse.hawkbit.repository.builder.RolloutCreate; @@ -42,6 +45,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; @@ -126,7 +130,6 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { final Action rolloutCreatedAction = actionsByKnownTarget.stream() .filter(action -> !action.getId().equals(manuallyAssignedActionId)).findAny().get(); assertThat(rolloutCreatedAction.getStatus()).isEqualTo(Status.FINISHED); - } @Test @@ -599,7 +602,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { final String successCondition = "50"; final String errorCondition = "80"; final Rollout createdRollout = createSimpleTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout, - amountOtherTargets, amountGroups, successCondition, errorCondition, ActionType.DOWNLOAD_ONLY); + amountOtherTargets, amountGroups, successCondition, errorCondition, ActionType.DOWNLOAD_ONLY, null); // targets have not started Map validationMap = createInitStatusMap(); @@ -619,7 +622,8 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { changeStatusForAllRunningActions(createdRollout, Status.DOWNLOADED); rolloutManagement.handleRollouts(); - // 4 targets are ready, 2 are finished(with DOWNLOADED action status) and 2 are running + // 4 targets are ready, 2 are finished(with DOWNLOADED action status) + // and 2 are running validationMap = createInitStatusMap(); validationMap.put(TotalTargetCountStatus.Status.SCHEDULED, 4L); validationMap.put(TotalTargetCountStatus.Status.FINISHED, 2L); @@ -628,7 +632,8 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { changeStatusForAllRunningActions(createdRollout, Status.DOWNLOADED); rolloutManagement.handleRollouts(); - // 2 targets are ready, 4 are finished(with DOWNLOADED action status) and 2 are running + // 2 targets are ready, 4 are finished(with DOWNLOADED action status) + // and 2 are running validationMap = createInitStatusMap(); validationMap.put(TotalTargetCountStatus.Status.SCHEDULED, 2L); validationMap.put(TotalTargetCountStatus.Status.FINISHED, 4L); @@ -637,7 +642,8 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { changeStatusForAllRunningActions(createdRollout, Status.DOWNLOADED); rolloutManagement.handleRollouts(); - // 0 targets are ready, 6 are finished(with DOWNLOADED action status) and 2 are running + // 0 targets are ready, 6 are finished(with DOWNLOADED action status) + // and 2 are running validationMap = createInitStatusMap(); validationMap.put(TotalTargetCountStatus.Status.FINISHED, 6L); validationMap.put(TotalTargetCountStatus.Status.RUNNING, 2L); @@ -645,7 +651,8 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { changeStatusForAllRunningActions(createdRollout, Status.FINISHED); rolloutManagement.handleRollouts(); - // 0 targets are ready, 6 are finished(with DOWNLOADED action status), 2 are finished and 0 are running + // 0 targets are ready, 6 are finished(with DOWNLOADED action status), 2 + // are finished and 0 are running validationMap = createInitStatusMap(); validationMap.put(TotalTargetCountStatus.Status.FINISHED, 8L); validateRolloutActionStatus(createdRollout.getId(), validationMap); @@ -1745,14 +1752,80 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { // verify that all running actions keep running assertThat(actionRepository.findByRolloutIdAndStatus(PAGE, deletedRollout.getId(), Status.RUNNING) .getNumberOfElements()).isEqualTo(2); + } + @Test + @Description("Creating a rollout without weight value when multi assignment in enabled.") + public void weightNotRequiredInMultiAssignmentMode() { + enableMultiAssignments(); + createSimpleTestRolloutWithTargetsAndDistributionSet(10, 10, 2, "50", "80", ActionType.FORCED, null); + } + + @Test + @Description("Creating a rollout with a weight causes an error when multi assignment in disabled.") + public void weightNotAllowedWhenMultiAssignmentModeNotEnabled() { + Assertions.assertThatExceptionOfType(MultiAssignmentIsNotEnabledException.class) + .isThrownBy(() -> createSimpleTestRolloutWithTargetsAndDistributionSet(10, 10, 2, "50", "80", + ActionType.FORCED, 66)); + } + + @Test + @Description("Weight is validated and saved to the Rollout.") + public void weightValidatedAndSaved() { + final String targetPrefix = UUID.randomUUID().toString(); + testdataFactory.createTargets(4, targetPrefix); + enableMultiAssignments(); + + Assertions.assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> createTestRolloutWithTargetsAndDistributionSet(4, 2, "50", "80", + UUID.randomUUID().toString(), UUID.randomUUID().toString(), Action.WEIGHT_MAX + 1)); + Assertions.assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> createTestRolloutWithTargetsAndDistributionSet(4, 2, "50", "80", + UUID.randomUUID().toString(), UUID.randomUUID().toString(), Action.WEIGHT_MIN - 1)); + final Rollout createdRollout1 = createTestRolloutWithTargetsAndDistributionSet(4, 2, "50", "80", + UUID.randomUUID().toString(), UUID.randomUUID().toString(), Action.WEIGHT_MAX); + final Rollout createdRollout2 = createTestRolloutWithTargetsAndDistributionSet(4, 2, "50", "80", + UUID.randomUUID().toString(), UUID.randomUUID().toString(), Action.WEIGHT_MIN); + assertThat(rolloutRepository.findById(createdRollout1.getId()).get().getWeight()).get() + .isEqualTo(Action.WEIGHT_MAX); + assertThat(rolloutRepository.findById(createdRollout2.getId()).get().getWeight()).get() + .isEqualTo(Action.WEIGHT_MIN); + } + + @Test + @Description("A Rollout with weight creats actions with weights") + public void actionsWithWeightAreCreated() { + final int amountOfTargets = 5; + final int weight = 99; + enableMultiAssignments(); + final Long rolloutId = createSimpleTestRolloutWithTargetsAndDistributionSet(amountOfTargets, 2, amountOfTargets, + "80", "50", null, weight).getId(); + rolloutManagement.start(rolloutId); + rolloutManagement.handleRollouts(); + final List actions = deploymentManagement.findActionsAll(PAGE).getContent(); + assertThat(actions).hasSize(amountOfTargets); + assertThat(actions).allMatch(action -> action.getWeight().get() == weight); + } + + @Test + @Description("Rollout can be created without weight in single assignment and be started in multi assignment") + public void createInSingleStartInMultiassigMode() { + final int amountOfTargets = 5; + final Long rolloutId = createSimpleTestRolloutWithTargetsAndDistributionSet(amountOfTargets, 2, amountOfTargets, + "80", "50", null, null).getId(); + + enableMultiAssignments(); + rolloutManagement.start(rolloutId); + rolloutManagement.handleRollouts(); + final List actions = deploymentManagement.findActionsAll(PAGE).getContent(); + assertThat(actions).hasSize(amountOfTargets); + assertThat(actions).allMatch(action -> !action.getWeight().isPresent()); } private RolloutGroupCreate generateRolloutGroup(final int index, final Integer percentage, final String targetFilter) { return entityFactory.rolloutGroup().create().name("Group" + index).description("Group" + index + "desc") .targetPercentage(Float.valueOf(percentage)).targetFilterQuery(targetFilter); - } private RolloutCreate generateTargetsAndRollout(final String rolloutName, final int amountTargetsForRollout) { @@ -1790,27 +1863,35 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { final int amountOtherTargets, final int groupSize, final String successCondition, final String errorCondition) { return createSimpleTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout, amountOtherTargets, - groupSize, successCondition, errorCondition, ActionType.FORCED); + groupSize, successCondition, errorCondition, ActionType.FORCED, null); } private Rollout createSimpleTestRolloutWithTargetsAndDistributionSet(final int amountTargetsForRollout, final int amountOtherTargets, final int groupSize, final String successCondition, - final String errorCondition, final ActionType actionType) { + final String errorCondition, final ActionType actionType, final Integer weight) { final DistributionSet rolloutDS = testdataFactory.createDistributionSet("rolloutDS"); testdataFactory.createTargets(amountTargetsForRollout, "rollout-", "rollout"); testdataFactory.createTargets(amountOtherTargets, "others-", "rollout"); final String filterQuery = "controllerId==rollout-*"; return testdataFactory.createRolloutByVariables("test-rollout-name-1", "test-rollout-description-1", groupSize, - filterQuery, rolloutDS, successCondition, errorCondition, actionType); + filterQuery, rolloutDS, successCondition, errorCondition, actionType, weight); } private Rollout createTestRolloutWithTargetsAndDistributionSet(final int amountTargetsForRollout, final int groupSize, final String successCondition, final String errorCondition, final String rolloutName, final String targetPrefixName) { + return createTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout, groupSize, successCondition, + errorCondition, rolloutName, targetPrefixName, null); + } + + private Rollout createTestRolloutWithTargetsAndDistributionSet(final int amountTargetsForRollout, + final int groupSize, final String successCondition, final String errorCondition, final String rolloutName, + final String targetPrefixName, final Integer weight) { final DistributionSet dsForRolloutTwo = testdataFactory.createDistributionSet("dsFor" + rolloutName); testdataFactory.createTargets(amountTargetsForRollout, targetPrefixName + "-", targetPrefixName); return testdataFactory.createRolloutByVariables(rolloutName, rolloutName + "description", groupSize, - "controllerId==" + targetPrefixName + "-*", dsForRolloutTwo, successCondition, errorCondition); + "controllerId==" + targetPrefixName + "-*", dsForRolloutTwo, successCondition, errorCondition, + Action.ActionType.FORCED, weight); } private int changeStatusForAllRunningActions(final Rollout rollout, final Status status) { 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 f6c91ff7b..7c976b512 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 @@ -20,6 +20,9 @@ import static org.junit.Assert.fail; import java.util.Arrays; import java.util.List; +import javax.validation.ConstraintViolationException; + +import org.assertj.core.api.Assertions; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; @@ -28,8 +31,10 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetFilterQueryCreat import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException; import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignDistributionSetException; +import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Target; @@ -82,13 +87,18 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest verifyThrownExceptionBy( () -> targetFilterQueryManagement.update(entityFactory.targetFilterQuery().update(NOT_EXIST_IDL)), "TargetFilterQuery"); - verifyThrownExceptionBy( - () -> targetFilterQueryManagement.updateAutoAssignDS(targetFilterQuery.getId(), NOT_EXIST_IDL), + + verifyThrownExceptionBy(() -> targetFilterQueryManagement.updateAutoAssignDS( + entityFactory.targetFilterQuery().updateAutoAssign(targetFilterQuery.getId()).ds(NOT_EXIST_IDL)), "DistributionSet"); - verifyThrownExceptionBy(() -> targetFilterQueryManagement.updateAutoAssignDS(NOT_EXIST_IDL, set.getId()), - "TargetFilterQuery"); + verifyThrownExceptionBy( - () -> targetFilterQueryManagement.updateAutoAssignDS(targetFilterQuery.getId(), NOT_EXIST_IDL), + () -> targetFilterQueryManagement.updateAutoAssignDS( + entityFactory.targetFilterQuery().updateAutoAssign(NOT_EXIST_IDL).ds(set.getId())), + "TargetFilterQuery"); + + verifyThrownExceptionBy(() -> targetFilterQueryManagement.updateAutoAssignDS( + entityFactory.targetFilterQuery().updateAutoAssign(targetFilterQuery.getId()).ds(NOT_EXIST_IDL)), "DistributionSet"); } @@ -208,23 +218,26 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest @Step private void verifyAutoAssignmentWithDefaultActionType(final String filterName, final TargetFilterQuery targetFilterQuery, final DistributionSet distributionSet) { - targetFilterQueryManagement.updateAutoAssignDS(targetFilterQuery.getId(), distributionSet.getId()); + targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() + .updateAutoAssign(targetFilterQuery.getId()).ds(distributionSet.getId())); verifyAutoAssignDsAndActionType(filterName, distributionSet, ActionType.FORCED); } @Step private void verifyAutoAssignmentWithSoftActionType(final String filterName, final TargetFilterQuery targetFilterQuery, final DistributionSet distributionSet) { - targetFilterQueryManagement.updateAutoAssignDSWithActionType(targetFilterQuery.getId(), distributionSet.getId(), - ActionType.SOFT); + targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() + .updateAutoAssign(targetFilterQuery.getId()).ds(distributionSet.getId()).actionType(ActionType.SOFT)); verifyAutoAssignDsAndActionType(filterName, distributionSet, ActionType.SOFT); } @Step private void verifyAutoAssignmentWithDownloadOnlyActionType(final String filterName, final TargetFilterQuery targetFilterQuery, final DistributionSet distributionSet) { - targetFilterQueryManagement.updateAutoAssignDSWithActionType(targetFilterQuery.getId(), distributionSet.getId(), - ActionType.DOWNLOAD_ONLY); + targetFilterQueryManagement + .updateAutoAssignDS(entityFactory.targetFilterQuery().updateAutoAssign(targetFilterQuery.getId()) + .ds(distributionSet.getId()).actionType(ActionType.DOWNLOAD_ONLY)); + verifyAutoAssignDsAndActionType(filterName, distributionSet, ActionType.DOWNLOAD_ONLY); } @@ -233,9 +246,11 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest final DistributionSet distributionSet) { // assigning a distribution set with TIMEFORCED action is supposed to // fail as only FORCED and SOFT action types are allowed - assertThatExceptionOfType(InvalidAutoAssignActionTypeException.class).isThrownBy( - () -> targetFilterQueryManagement.updateAutoAssignDSWithActionType(targetFilterQuery.getId(), - distributionSet.getId(), ActionType.TIMEFORCED)); + + assertThatExceptionOfType(InvalidAutoAssignActionTypeException.class) + .isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS( + entityFactory.targetFilterQuery().updateAutoAssign(targetFilterQuery.getId()) + .ds(distributionSet.getId()).actionType(ActionType.TIMEFORCED))); } @Step @@ -245,8 +260,8 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest .type(testdataFactory.findOrCreateDefaultTestDsType())); assertThatExceptionOfType(InvalidAutoAssignDistributionSetException.class) - .isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(targetFilterQuery.getId(), - incompleteDistributionSet.getId())); + .isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() + .updateAutoAssign(targetFilterQuery.getId()).ds(incompleteDistributionSet.getId()))); } @Step @@ -255,8 +270,9 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest assignDistributionSet(softDeletedDs, testdataFactory.createTarget("forSoftDeletedDs")); distributionSetManagement.delete(softDeletedDs.getId()); - assertThatExceptionOfType(InvalidAutoAssignDistributionSetException.class).isThrownBy( - () -> targetFilterQueryManagement.updateAutoAssignDS(targetFilterQuery.getId(), softDeletedDs.getId())); + assertThatExceptionOfType(InvalidAutoAssignDistributionSetException.class) + .isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() + .updateAutoAssign(targetFilterQuery.getId()).ds(softDeletedDs.getId()))); } private void verifyAutoAssignDsAndActionType(final String filterName, final DistributionSet distributionSet, @@ -282,8 +298,10 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest // assigning a distribution set is supposed to fail as the query // addresses too many targets - assertThatExceptionOfType(AssignmentQuotaExceededException.class).isThrownBy(() -> targetFilterQueryManagement - .updateAutoAssignDS(targetFilterQuery.getId(), distributionSet.getId())); + + assertThatExceptionOfType(AssignmentQuotaExceededException.class) + .isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() + .updateAutoAssign(targetFilterQuery.getId()).ds(distributionSet.getId()))); } @Test @@ -313,7 +331,8 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest final DistributionSet distributionSet = testdataFactory.createDistributionSet(); - targetFilterQueryManagement.updateAutoAssignDS(targetFilterQuery.getId(), distributionSet.getId()); + targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() + .updateAutoAssign(targetFilterQuery.getId()).ds(distributionSet.getId())); // Check if target filter query is there TargetFilterQuery tfq = targetFilterQueryManagement.getByName(filterName).get(); @@ -340,9 +359,11 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest // later step assignDistributionSet(distributionSet.getId(), target.getControllerId()); - targetFilterQueryManagement.updateAutoAssignDS(targetFilterQueryManagement + final Long filterId = targetFilterQueryManagement .create(entityFactory.targetFilterQuery().create().name(filterName).query("name==PendingTargets001")) - .getId(), distributionSet.getId()); + .getId(); + targetFilterQueryManagement.updateAutoAssignDS( + entityFactory.targetFilterQuery().updateAutoAssign(filterId).ds(distributionSet.getId())); // Check if target filter query is there with the distribution set TargetFilterQuery tfq = targetFilterQueryManagement.getByName(filterName).get(); @@ -372,20 +393,18 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest final DistributionSet distributionSet = testdataFactory.createDistributionSet(); final DistributionSet distributionSet2 = testdataFactory.createDistributionSet("2"); - final TargetFilterQuery tfq = targetFilterQueryManagement.updateAutoAssignDSWithActionType( - targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name("c").query("name==x")).getId(), - distributionSet.getId(), ActionType.SOFT); - final TargetFilterQuery tfq2 = targetFilterQueryManagement.updateAutoAssignDS( - targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name(filterName).query("name==z*")).getId(), - distributionSet2.getId()); + final TargetFilterQuery tfq = targetFilterQueryManagement + .create(entityFactory.targetFilterQuery().create().name("c").query("name==x") + .autoAssignDistributionSet(distributionSet).autoAssignActionType(ActionType.SOFT)); + final TargetFilterQuery tfq2 = targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create() + .name(filterName).query("name==z*").autoAssignDistributionSet(distributionSet2)); assertEquals(4L, targetFilterQueryManagement.count()); // check if find works verifyFindByDistributionSetAndRsql(distributionSet, null, tfq); - targetFilterQueryManagement.updateAutoAssignDS(tfq2.getId(), distributionSet.getId()); + targetFilterQueryManagement.updateAutoAssignDS( + entityFactory.targetFilterQuery().updateAutoAssign(tfq2.getId()).ds(distributionSet.getId())); // check if find works for two verifyFindByDistributionSetAndRsql(distributionSet, null, tfq, tfq2); @@ -421,4 +440,73 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest verifyExpectedFilterQueriesInList(tfqList, expectedFilterQueries); } + + @Test + @Description("Creating or updating a target filter query with autoassignment and no-value weight when multi assignment in enabled.") + public void weightNotRequiredInMultiAssignmentMode() { + enableMultiAssignments(); + final DistributionSet ds = testdataFactory.createDistributionSet(); + final Long filterId = targetFilterQueryManagement + .create(entityFactory.targetFilterQuery().create().name("a").query("name==*")).getId(); + + targetFilterQueryManagement.create( + entityFactory.targetFilterQuery().create().name("b").query("name==*").autoAssignDistributionSet(ds)); + targetFilterQueryManagement + .updateAutoAssignDS(entityFactory.targetFilterQuery().updateAutoAssign(filterId).ds(ds.getId())); + } + + @Test + @Description("Creating or updating a target filter query with autoassignment with a weight causes an error when multi assignment in disabled.") + public void weightNotAllowedWhenMultiAssignmentModeNotEnabled() { + final DistributionSet ds = testdataFactory.createDistributionSet(); + final Long filterId = targetFilterQueryManagement + .create(entityFactory.targetFilterQuery().create().name("a").query("name==*")).getId(); + + Assertions.assertThatExceptionOfType(MultiAssignmentIsNotEnabledException.class) + .isThrownBy(() -> targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create() + .name("b").query("name==*").autoAssignDistributionSet(ds).autoAssignWeight(342))); + Assertions.assertThatExceptionOfType(MultiAssignmentIsNotEnabledException.class) + .isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS( + entityFactory.targetFilterQuery().updateAutoAssign(filterId).ds(ds.getId()).weight(343))); + } + + @Test + @Description("Auto assignment can be removed from filter when multi assignment in enabled.") + public void removeDsFromFilterWhenMultiAssignmentModeNotEnabled() { + enableMultiAssignments(); + final DistributionSet ds = testdataFactory.createDistributionSet(); + final Long filterId = targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("a") + .query("name==*").autoAssignDistributionSet(ds).autoAssignWeight(23)).getId(); + targetFilterQueryManagement + .updateAutoAssignDS(entityFactory.targetFilterQuery().updateAutoAssign(filterId).ds(null).weight(null)); + } + + @Test + @Description("Weight is validated and saved to the Filter.") + public void weightValidatedAndSaved() { + enableMultiAssignments(); + final DistributionSet ds = testdataFactory.createDistributionSet(); + + Assertions.assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy( + () -> targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("a") + .query("name==*").autoAssignDistributionSet(ds).autoAssignWeight(Action.WEIGHT_MAX + 1))); + + final Long filterId = targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("a") + .query("name==*").autoAssignDistributionSet(ds).autoAssignWeight(Action.WEIGHT_MAX)).getId(); + assertThat(targetFilterQueryManagement.get(filterId).get().getAutoAssignWeight().get()) + .isEqualTo(Action.WEIGHT_MAX); + + Assertions.assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() + .updateAutoAssign(filterId).ds(ds.getId()).weight(Action.WEIGHT_MAX + 1))); + Assertions.assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() + .updateAutoAssign(filterId).ds(ds.getId()).weight(Action.WEIGHT_MIN - 1))); + targetFilterQueryManagement.updateAutoAssignDS( + entityFactory.targetFilterQuery().updateAutoAssign(filterId).ds(ds.getId()).weight(Action.WEIGHT_MAX)); + targetFilterQueryManagement.updateAutoAssignDS( + entityFactory.targetFilterQuery().updateAutoAssign(filterId).ds(ds.getId()).weight(Action.WEIGHT_MIN)); + assertThat(targetFilterQueryManagement.get(filterId).get().getAutoAssignWeight().get()) + .isEqualTo(Action.WEIGHT_MIN); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementSearchTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementSearchTest.java index 466437030..b64476c61 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementSearchTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementSearchTest.java @@ -131,7 +131,8 @@ public class TargetManagementSearchTest extends AbstractJpaIntegrationTest { // try to find several targets with different filter settings verifyThat1TargetHasNameAndId("targ-A-special", targSpecialName.getControllerId()); verifyThat1TargetHasAttributeValue("%c-attribute%", targAttribute.getControllerId()); - verifyThat1TargetHasAttributeValue("%" + targAttributeId.getControllerId() + "%", targAttributeId.getControllerId()); + verifyThat1TargetHasAttributeValue("%" + targAttributeId.getControllerId() + "%", + targAttributeId.getControllerId()); verifyThatRepositoryContains400Targets(); verifyThat200TargetsHaveTagD(targTagW, concat(targBs, targCs)); verifyThat100TargetsContainsGivenTextAndHaveTagAssigned(targTagY, targTagW, targBs); 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 207763790..9327c15ea 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 @@ -63,8 +63,8 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest { // target filter query that matches all targets final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement .create(entityFactory.targetFilterQuery().create().name("filterA").query("name==*")); - targetFilterQueryManagement.updateAutoAssignDS(targetFilterQuery.getId(), secondDistributionSet.getId()); - + targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() + .updateAutoAssign(targetFilterQuery.getId()).ds(secondDistributionSet.getId())); // Run the check autoAssignChecker.check(); @@ -101,7 +101,8 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest { // target filter query that matches all targets final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement .create(entityFactory.targetFilterQuery().create().name("filterA").query("name==*")); - targetFilterQueryManagement.updateAutoAssignDS(targetFilterQuery.getId(), setA.getId()); + targetFilterQueryManagement.updateAutoAssignDS( + entityFactory.targetFilterQuery().updateAutoAssign(targetFilterQuery.getId()).ds(setA.getId())); final String targetDsAIdPref = "targ"; final List targets = testdataFactory.createTargets(100, targetDsAIdPref, @@ -150,18 +151,16 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest { // target filter query that matches first bunch of targets, that should // fail - assertThatExceptionOfType( - InvalidAutoAssignDistributionSetException.class) - .isThrownBy( - () -> targetFilterQueryManagement.updateAutoAssignDS( - targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create() - .name("filterA").query("id==" + targetDsFIdPref + "*")).getId(), - setF.getId())); - + assertThatExceptionOfType(InvalidAutoAssignDistributionSetException.class).isThrownBy(() -> { + final Long filterId = targetFilterQueryManagement.create( + entityFactory.targetFilterQuery().create().name("filterA").query("id==" + targetDsFIdPref + "*")) + .getId(); + targetFilterQueryManagement + .updateAutoAssignDS(entityFactory.targetFilterQuery().updateAutoAssign(filterId).ds(setF.getId())); + }); // target filter query that matches failed bunch of targets - targetFilterQueryManagement.updateAutoAssignDS(targetFilterQueryManagement.create( - entityFactory.targetFilterQuery().create().name("filterB").query("id==" + targetDsAIdPref + "*")) - .getId(), setA.getId()); + targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("filterB") + .query("id==" + targetDsAIdPref + "*").autoAssignDistributionSet(setA.getId())); final List targetsF = testdataFactory.createTargets(10, targetDsFIdPref, targetDsFIdPref.concat(" description")); @@ -243,12 +242,10 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest { final List targets = testdataFactory.createTargets(targetCount, "target" + prefix, prefix.concat(" description")); + targetFilterQueryManagement.create( + entityFactory.targetFilterQuery().create().name("filter" + prefix).query("id==target" + prefix + "*") + .autoAssignDistributionSet(distributionSet).autoAssignActionType(actionType)); - targetFilterQueryManagement - .updateAutoAssignDSWithActionType( - targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create() - .name("filter" + prefix).query("id==target" + prefix + "*")).getId(), - distributionSet.getId(), actionType); return targets; } @@ -262,4 +259,38 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest { assertThat(actions).allMatch(action -> action.getActionType().equals(actionType)); } + @Test + @Description("An auto assignment target filter with weight creats actions with weights") + public void actionsWithWeightAreCreated() throws Exception { + final int amountOfTargets = 5; + final DistributionSet ds = testdataFactory.createDistributionSet(); + final int weight = 32; + enableMultiAssignments(); + + targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("a").query("name==*") + .autoAssignDistributionSet(ds).autoAssignWeight(weight)); + testdataFactory.createTargets(amountOfTargets); + autoAssignChecker.check(); + + final List actions = deploymentManagement.findActionsAll(PAGE).getContent(); + assertThat(actions).hasSize(amountOfTargets); + assertThat(actions).allMatch(action -> action.getWeight().get() == weight); + } + + @Test + @Description("An auto assignment target filter without weight still works after multi assignment is enabled") + public void filterWithoutWeightWorksInMultiAssignmentMode() throws Exception { + final int amountOfTargets = 5; + final DistributionSet ds = testdataFactory.createDistributionSet(); + targetFilterQueryManagement.create( + entityFactory.targetFilterQuery().create().name("a").query("name==*").autoAssignDistributionSet(ds)); + enableMultiAssignments(); + + testdataFactory.createTargets(amountOfTargets); + autoAssignChecker.check(); + + final List actions = deploymentManagement.findActionsAll(PAGE).getContent(); + assertThat(actions).hasSize(amountOfTargets); + assertThat(actions).allMatch(action -> !action.getWeight().isPresent()); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLActionFieldsTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLActionFieldsTest.java index 919534b0e..de3918da4 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLActionFieldsTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLActionFieldsTest.java @@ -46,6 +46,7 @@ public class RSQLActionFieldsTest extends AbstractJpaIntegrationTest { action.setDistributionSet(dsA); action.setTarget(target); action.setStatus(Status.RUNNING); + action.setWeight(45); target.addAction(action); actionRepository.save(action); @@ -56,6 +57,7 @@ public class RSQLActionFieldsTest extends AbstractJpaIntegrationTest { newAction.setActive(i % 2 == 0); newAction.setStatus(Status.RUNNING); newAction.setTarget(target); + newAction.setWeight(45); actionRepository.save(newAction); target.addAction(newAction); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFilterQueryFieldsTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFilterQueryFieldsTest.java index 17d9dacdc..1490998ef 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFilterQueryFieldsTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFilterQueryFieldsTest.java @@ -41,14 +41,10 @@ public class RSQLTargetFilterQueryFieldsTest extends AbstractJpaIntegrationTest final DistributionSet ds1 = testdataFactory.createDistributionSet("AutoAssignedDs_1"); final DistributionSet ds2 = testdataFactory.createDistributionSet("AutoAssignedDs_2"); - filter1 = targetFilterQueryManagement.updateAutoAssignDSWithActionType( - targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name(filterName1).query("name==*")).getId(), - ds1.getId(), ActionType.SOFT); - filter2 = targetFilterQueryManagement.updateAutoAssignDS( - targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name(filterName2).query("name==*")).getId(), - ds2.getId()); + filter1 = targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name(filterName1) + .query("name==*").autoAssignDistributionSet(ds1).autoAssignActionType(ActionType.SOFT)); + filter2 = targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name(filterName2) + .query("name==*").autoAssignDistributionSet(ds2)); targetFilterQueryManagement .create(entityFactory.targetFilterQuery().create().name(filterName3).query("name==*")); 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 4b02fcc99..e2c74b99d 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 @@ -16,9 +16,7 @@ import java.io.File; import java.io.IOException; import java.net.URI; import java.time.ZonedDateTime; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -118,6 +116,8 @@ public abstract class AbstractIntegrationTest { protected static final URI LOCALHOST = URI.create("http://127.0.0.1"); + protected static final int DEFAULT_TEST_WEIGHT = 500; + /** * Number of {@link DistributionSetType}s that exist in every test case. One * generated by using @@ -250,9 +250,14 @@ public abstract class AbstractIntegrationTest { protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final List controllerIds, final ActionType actionType, final long forcedTime) { + return assignDistributionSet(dsID, controllerIds, actionType, forcedTime, null); + } + + protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final List controllerIds, + final ActionType actionType, final long forcedTime, final Integer weight) { final List deploymentRequests = controllerIds.stream() .map(id -> DeploymentManagement.deploymentRequest(id, dsID).setActionType(actionType) - .setForceTime(forcedTime).build()) + .setForceTime(forcedTime).setWeight(weight).build()) .collect(Collectors.toList()); final List results = deploymentManagement .assignDistributionSets(deploymentRequests); @@ -266,7 +271,13 @@ public abstract class AbstractIntegrationTest { return assignDistributionSet(ds.getId(), targetIds, ActionType.FORCED); } - private DistributionSetAssignmentResult makeAssignment(final DeploymentRequest request) { + protected DistributionSetAssignmentResult assignDistributionSet(final Long dsId, final List targetIds, + final int weight) { + return assignDistributionSet(dsId, targetIds, ActionType.FORCED, RepositoryModelConstants.NO_FORCE_TIME, + weight); + } + + protected DistributionSetAssignmentResult makeAssignment(final DeploymentRequest request) { final List results = deploymentManagement .assignDistributionSets(Collections.singletonList(request)); assertThat(results).hasSize(1); @@ -302,35 +313,21 @@ public abstract class AbstractIntegrationTest { protected DistributionSetAssignmentResult assignDistributionSetWithMaintenanceWindow(final long dsID, final String controllerId, final String maintenanceWindowSchedule, final String maintenanceWindowDuration, final String maintenanceWindowTimeZone) { - + return makeAssignment(DeploymentManagement.deploymentRequest(controllerId, dsID) .setMaintenance(maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone) .build()); } - protected DistributionSetAssignmentResult assignDistributionSetWithMaintenanceWindow(final long dsID, - final String controllerId, final ActionType type, final String maintenanceWindowSchedule, - final String maintenanceWindowDuration, - final String maintenanceWindowTimeZone) { - return makeAssignment(DeploymentManagement.deploymentRequest(controllerId, dsID).setActionType(type) - .setMaintenance(maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone) - .build()); - } - - protected List createAssignmentRequests(final Collection distributionSets, - final Collection targets) { - final List deploymentRequests = new ArrayList<>(); - distributionSets.forEach(ds -> targets.forEach( - target -> deploymentRequests - .add(DeploymentManagement.deploymentRequest(target.getControllerId(), ds.getId()).build())) - ); - return deploymentRequests; - } - protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet pset, final Target target) { return assignDistributionSet(pset, Arrays.asList(target)); } + protected DistributionSetAssignmentResult assignDistributionSet(final long dsId, final String targetId, + final int weight) { + return assignDistributionSet(dsId, Collections.singletonList(targetId), weight); + } + protected void enableMultiAssignments() { tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, true); } @@ -477,12 +474,14 @@ public abstract class AbstractIntegrationTest { return randomStringBuilder.toString(); } - protected static Action getFirstAssignedAction(final DistributionSetAssignmentResult distributionSetAssignmentResult) { + protected static Action getFirstAssignedAction( + final DistributionSetAssignmentResult distributionSetAssignmentResult) { return distributionSetAssignmentResult.getAssignedEntity().stream().findFirst() .orElseThrow(() -> new IllegalStateException("expected one assigned action, found none")); } - protected static Long getFirstAssignedActionId(final DistributionSetAssignmentResult distributionSetAssignmentResult) { + protected static Long getFirstAssignedActionId( + final DistributionSetAssignmentResult distributionSetAssignmentResult) { return getFirstAssignedAction(distributionSetAssignmentResult).getId(); } 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 5d1a38a46..389f3f358 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 @@ -1,6 +1,6 @@ /** * Copyright (c) 2015 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 @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -117,8 +118,8 @@ public class TestdataFactory { public static final String SM_TYPE_RT = "runtime"; /** - * Key of test "application" {@link SoftwareModuleType} : optional software in - * {@link #DS_TYPE_DEFAULT}. + * Key of test "application" {@link SoftwareModuleType} : optional software + * in {@link #DS_TYPE_DEFAULT}. */ public static final String SM_TYPE_APP = "application"; @@ -160,13 +161,14 @@ 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} and + * {@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) { @@ -175,24 +177,25 @@ 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} and + * {@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() { - return createDistributionSet("", DEFAULT_VERSION, false); + return createDistributionSet(UUID.randomUUID().toString(), DEFAULT_VERSION, false); } /** * 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} and + * {@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) { @@ -201,15 +204,16 @@ 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} and + * {@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) { @@ -218,14 +222,15 @@ 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}. - * + * {@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) { @@ -234,15 +239,16 @@ 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} and + * {@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) { @@ -251,17 +257,19 @@ 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}. - * + * {@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. * @param version * {@link DistributionSet#getVersion()} and - * {@link SoftwareModule#getVersion()} extended by a random number. + * {@link SoftwareModule#getVersion()} extended by a random + * number. * @param isRequiredMigrationStep * for {@link DistributionSet#isRequiredMigrationStep()} + * * @return {@link DistributionSet} entity. */ public DistributionSet createDistributionSet(final String prefix, final String version, @@ -295,7 +303,7 @@ public class TestdataFactory { * {@link SoftwareModuleMetadata#isTargetVisible()} and * {@link #INVISIBLE_SM_MD_KEY}, {@link #INVISIBLE_SM_MD_VALUE} without * {@link SoftwareModuleMetadata#isTargetVisible()} - * + * * @param set * to add metadata to */ @@ -313,17 +321,19 @@ public class TestdataFactory { /** * Creates {@link DistributionSet} in repository. - * + * * @param prefix * for {@link SoftwareModule}s and {@link DistributionSet}s name, * vendor and description. * @param version * {@link DistributionSet#getVersion()} and - * {@link SoftwareModule#getVersion()} extended by a random number. + * {@link SoftwareModule#getVersion()} extended by a random + * number. * @param isRequiredMigrationStep * for {@link DistributionSet#isRequiredMigrationStep()} * @param modules * for {@link DistributionSet#getModules()} + * * @return {@link DistributionSet} entity. */ public DistributionSet createDistributionSet(final String prefix, final String version, @@ -338,9 +348,9 @@ 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}. - * + * {@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. @@ -350,6 +360,7 @@ public class TestdataFactory { * number.updat * @param tags * {@link DistributionSet#getTags()} + * * @return {@link DistributionSet} entity. */ public DistributionSet createDistributionSet(final String prefix, final String version, @@ -365,13 +376,14 @@ public class TestdataFactory { /** * Creates {@link DistributionSet}s 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} followed by an iterative - * number and {@link DistributionSet#isRequiredMigrationStep()} + * {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} + * , {@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) { @@ -380,8 +392,9 @@ public class TestdataFactory { } /** - * Create a list of {@link DistributionSet}s without modules, i.e. incomplete. - * + * 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 @@ -400,16 +413,17 @@ public class TestdataFactory { /** * Creates {@link DistributionSet}s 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} followed by an iterative - * number and {@link DistributionSet#isRequiredMigrationStep()} + * {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} + * , {@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) { @@ -426,11 +440,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) { @@ -440,11 +455,12 @@ public class TestdataFactory { } /** - * Creates {@link Artifact}s for given {@link SoftwareModule} with a small text - * payload. - * + * 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) { @@ -458,15 +474,18 @@ public class TestdataFactory { } /** - * Create an {@link Artifact} for given {@link SoftwareModule} with a small text - * payload. - * + * 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) { @@ -476,17 +495,21 @@ public class TestdataFactory { } /** - * Create an {@link Artifact} for given {@link SoftwareModule} with a small text - * payload. + * 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. + * * @param fileSize * the file size + * * @return {@link Artifact} entity. */ public Artifact createArtifact(final byte[] artifactData, final Long moduleId, final String filename, @@ -497,11 +520,12 @@ public class TestdataFactory { /** * Creates {@link SoftwareModule} with {@link #DEFAULT_VENDOR} and - * {@link #DEFAULT_VERSION} and random generated {@link Target#getDescription()} - * in the repository. - * + * {@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) { @@ -509,10 +533,12 @@ public class TestdataFactory { } /** - * Creates {@link SoftwareModule} of type {@value Constants#SMT_DEFAULT_APP_KEY} - * with {@link #DEFAULT_VENDOR} and {@link #DEFAULT_VERSION} and random - * generated {@link Target#getDescription()} in the repository. - * + * Creates {@link SoftwareModule} of type + * {@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() { @@ -520,12 +546,15 @@ public class TestdataFactory { } /** - * Creates {@link SoftwareModule} of type {@value Constants#SMT_DEFAULT_APP_KEY} - * with {@link #DEFAULT_VENDOR} and {@link #DEFAULT_VERSION} and random - * generated {@link Target#getDescription()} in the repository. - * + * Creates {@link SoftwareModule} of type + * {@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) { @@ -533,10 +562,12 @@ public class TestdataFactory { } /** - * Creates {@link SoftwareModule} of type {@value Constants#SMT_DEFAULT_OS_KEY} - * with {@link #DEFAULT_VENDOR} and {@link #DEFAULT_VERSION} and random - * generated {@link Target#getDescription()} in the repository. - * + * Creates {@link SoftwareModule} of type + * {@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() { @@ -544,12 +575,15 @@ public class TestdataFactory { } /** - * Creates {@link SoftwareModule} of type {@value Constants#SMT_DEFAULT_OS_KEY} - * with {@link #DEFAULT_VENDOR} and {@link #DEFAULT_VERSION} and random - * generated {@link Target#getDescription()} in the repository. - * + * Creates {@link SoftwareModule} of type + * {@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) { @@ -558,13 +592,14 @@ public class TestdataFactory { /** * Creates {@link SoftwareModule} with {@link #DEFAULT_VENDOR} and - * {@link #DEFAULT_VERSION} and random generated {@link Target#getDescription()} - * in the repository. - * + * {@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) { @@ -603,7 +638,7 @@ public class TestdataFactory { return target; } - private void assertTargetProperlyCreated(Target target) { + private void assertTargetProperlyCreated(final Target target) { assertThat(target.getCreatedBy()).isNotNull(); assertThat(target.getCreatedAt()).isNotNull(); assertThat(target.getLastModifiedBy()).isNotNull(); @@ -614,15 +649,16 @@ public class TestdataFactory { /** * Creates {@link DistributionSet}s 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} followed by an iterative - * number and {@link DistributionSet#isRequiredMigrationStep()} + * {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} + * , {@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. - * + * {@link SoftwareModule}s to ensure that + * {@link BaseEntity#getLastModifiedAt()} and + * {@link BaseEntity#getLastModifiedBy()} is filled. + * * @return persisted {@link DistributionSet}. */ public DistributionSet createUpdatedDistributionSet() { @@ -639,8 +675,8 @@ public class TestdataFactory { /** * @return {@link DistributionSetType} with key {@link #DS_TYPE_DEFAULT} and - * {@link SoftwareModuleType}s {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} - * , {@link #SM_TYPE_APP}. + * {@link SoftwareModuleType}s {@link #SM_TYPE_OS}, + * {@link #SM_TYPE_RT} , {@link #SM_TYPE_APP}. */ public DistributionSetType findOrCreateDefaultTestDsType() { final List mand = new ArrayList<>(); @@ -656,11 +692,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) { @@ -672,7 +709,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 @@ -681,6 +718,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, @@ -694,11 +732,12 @@ 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. - * + * {@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) { @@ -708,11 +747,12 @@ 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) { @@ -732,6 +772,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, @@ -754,6 +795,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, @@ -775,6 +817,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, @@ -787,6 +830,7 @@ public class TestdataFactory { * * @param name * {@link DistributionSet#getName()} + * * @return the generated {@link DistributionSet} */ public DistributionSet generateDistributionSet(final String name) { @@ -795,11 +839,13 @@ public class TestdataFactory { } /** - * Creates {@link Target}s in repository and with {@link #DEFAULT_CONTROLLER_ID} - * as prefix for {@link Target#getControllerId()}. - * + * 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) { @@ -815,7 +861,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 @@ -835,8 +881,9 @@ public class TestdataFactory { /** * Builds {@link Target} objects with given prefix for - * {@link Target#getControllerId()} followed by a number suffix starting with 0. - * + * {@link Target#getControllerId()} followed by a number suffix starting + * with 0. + * * @param numberOfTargets * of {@link Target}s to generate * @param controllerIdPrefix @@ -926,9 +973,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) { @@ -950,14 +998,16 @@ public class TestdataFactory { } /** - * Append {@link ActionStatus} to all {@link Action}s of given {@link Target}s. - * + * 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, @@ -966,14 +1016,16 @@ public class TestdataFactory { } /** - * Append {@link ActionStatus} to all {@link Action}s of given {@link Target}s. - * + * 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, @@ -991,7 +1043,7 @@ public class TestdataFactory { /** * Creates rollout based on given parameters. - * + * * @param rolloutName * of the {@link Rollout} * @param rolloutDescription @@ -1012,7 +1064,7 @@ public class TestdataFactory { final int groupSize, final String filterQuery, final DistributionSet distributionSet, final String successCondition, final String errorCondition) { return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet, - successCondition, errorCondition, Action.ActionType.FORCED); + successCondition, errorCondition, Action.ActionType.FORCED, null); } /** @@ -1034,11 +1086,14 @@ public class TestdataFactory { * to switch to next group * @param actionType * the type of the Rollout + * @param weight + * weight of the Rollout * @return created {@link Rollout} */ public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription, final int groupSize, final String filterQuery, final DistributionSet distributionSet, - final String successCondition, final String errorCondition, final Action.ActionType actionType) { + final String successCondition, final String errorCondition, final Action.ActionType actionType, + final Integer weight) { final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults() .successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition) .errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition) @@ -1046,7 +1101,7 @@ public class TestdataFactory { final Rollout rollout = rolloutManagement.create( entityFactory.rollout().create().name(rolloutName).description(rolloutDescription) - .targetFilterQuery(filterQuery).set(distributionSet).actionType(actionType), + .targetFilterQuery(filterQuery).set(distributionSet).actionType(actionType).weight(weight), groupSize, conditions); // Run here, because Scheduler is disabled during tests @@ -1058,10 +1113,10 @@ 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 + * for rollouts name, description, + * {@link Target#getControllerId()} filter * @return created {@link Rollout} */ public Rollout createRollout(final String prefix) { @@ -1071,12 +1126,12 @@ public class TestdataFactory { } /** - * Create the soft deleted {@link Rollout} with a new {@link DistributionSet} - * and {@link Target}s. - * + * 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 + * for rollouts name, description, + * {@link Target#getControllerId()} filter * @return created {@link Rollout} */ public Rollout createSoftDeletedRollout(final String prefix) { diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java b/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java index d3c48b5a4..8367bc904 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java @@ -149,7 +149,7 @@ public class DdiRootController implements DdiRootControllerRestApi { final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, IpUtil .getClientIpFromRequest(requestResponseContextHolder.getHttpServletRequest(), securityProperties)); - final Action action = controllerManagement.findOldestActiveActionByTarget(controllerId).orElse(null); + final Action action = controllerManagement.findActiveActionWithHighestWeight(controllerId).orElse(null); checkAndCancelExpiredAction(action); diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java index 057c2501f..50666258e 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java @@ -105,12 +105,12 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Ensures that targets cannot be created e.g. in plug'n play scenarios when tenant does not exists but can be created if the tenant exists.") - @WithUser(tenantId = "tenantDoesNotExists", allSpPermissions = true, authorities = {CONTROLLER_ROLE, - SYSTEM_ROLE}, autoCreateTenant = false) - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), + @WithUser(tenantId = "tenantDoesNotExists", allSpPermissions = true, authorities = { CONTROLLER_ROLE, + SYSTEM_ROLE }, autoCreateTenant = false) + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1), @Expect(type = DistributionSetTypeCreatedEvent.class, count = 3), - @Expect(type = SoftwareModuleTypeCreatedEvent.class, count = 2)}) + @Expect(type = SoftwareModuleTypeCreatedEvent.class, count = 2) }) public void targetCannotBeRegisteredIfTenantDoesNotExistsButWhenExists() throws Exception { mvc.perform(get("/default-tenant/", tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print()) @@ -131,10 +131,10 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Ensures that target poll request does not change audit data on the entity.") - @WithUser(principal = "knownPrincipal", authorities = {SpPermission.READ_TARGET, SpPermission.UPDATE_TARGET, - SpPermission.CREATE_TARGET}, allSpPermissions = false) - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1)}) + @WithUser(principal = "knownPrincipal", authorities = { SpPermission.READ_TARGET, SpPermission.UPDATE_TARGET, + SpPermission.CREATE_TARGET }, allSpPermissions = false) + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) }) public void targetPollDoesNotModifyAuditData() throws Exception { // create target first with "knownPrincipal" user and audit data final String knownTargetControllerId = "target1"; @@ -162,15 +162,15 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Ensures that server returns a not found response in case of empty controlloer ID.") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 0)}) + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) }) public void rootRsWithoutId() throws Exception { mvc.perform(get("/controller/v1/")).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); } @Test @Description("Ensures that the system creates a new target in plug and play manner, i.e. target is authenticated but does not exist yet.") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 1)}) + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1) }) public void rootRsPlugAndPlay() throws Exception { final long current = System.currentTimeMillis(); @@ -198,8 +198,8 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Ensures that tenant specific polling time, which is saved in the db, is delivered to the controller.") @WithUser(principal = "knownpricipal", allSpPermissions = false) - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 1)}) + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1) }) public void pollWithModifiedGloablPollingTime() throws Exception { securityRule.runAs(WithSpringAuthorityRule.withUser("tenantadmin", HAS_AUTH_TENANT_CONFIGURATION), () -> { tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.POLLING_TIME_INTERVAL, @@ -218,14 +218,14 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Ensures that etag check results in not modified response if provided etag by client is identical to entity in repository.") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 6), @Expect(type = TargetAssignDistributionSetEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 3), @Expect(type = ActionUpdatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 2), @Expect(type = ActionCreatedEvent.class, count = 2), @Expect(type = TargetAttributesRequestedEvent.class, count = 1), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 6)}) + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6) }) public void rootRsNotModified() throws Exception { final String etag = mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) @@ -289,8 +289,8 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Ensures that the target state machine of a precomissioned target switches from " + "UNKNOWN to REGISTERED when the target polls for the first time.") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1)}) + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) }) public void rootRsPrecommissioned() throws Exception { testdataFactory.createTarget("4711"); @@ -314,8 +314,8 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Ensures that the source IP address of the polling target is correctly stored in repository") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 1)}) + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1) }) public void rootRsPlugAndPlayIpAddress() throws Exception { // test final String knownControllerId1 = "0815"; @@ -341,8 +341,8 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Ensures that the source IP address of the polling target is not stored in repository if disabled") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 1)}) + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1) }) public void rootRsIpAddressNotStoredIfDisabled() throws Exception { securityProperties.getClients().setTrackRemoteIp(false); @@ -360,12 +360,12 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Controller trys to finish an update process after it has been finished by an error action status.") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1), @Expect(type = TargetUpdatedEvent.class, count = 2), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) public void tryToFinishAnUpdateProcessAfterItHasBeenFinished() throws Exception { final DistributionSet ds = testdataFactory.createDistributionSet(""); Target savedTarget = testdataFactory.createTarget("911"); @@ -384,13 +384,13 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Controller sends attribute update request after device successfully closed software update.") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 2), @Expect(type = ActionCreatedEvent.class, count = 2), @Expect(type = ActionUpdatedEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 6), @Expect(type = TargetPollEvent.class, count = 4), @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), - @Expect(type = TargetAttributesRequestedEvent.class, count = 1)}) + @Expect(type = TargetAttributesRequestedEvent.class, count = 1) }) public void attributeUpdateRequestSendingAfterSuccessfulDeployment() throws Exception { final DistributionSet ds = testdataFactory.createDistributionSet("1"); final Target savedTarget = testdataFactory.createTarget("922"); @@ -463,13 +463,13 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Test to verify that only a specific count of messages are returned based on the input actionHistory for getControllerDeploymentActionFeedback endpoint.") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 2), @Expect(type = TargetAttributesRequestedEvent.class, count = 1), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) public void testActionHistoryCount() throws Exception { final DistributionSet ds = testdataFactory.createDistributionSet(""); Target savedTarget = testdataFactory.createTarget("911"); @@ -498,13 +498,13 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Test to verify that a zero input value of actionHistory results in no action history appended for getControllerDeploymentActionFeedback endpoint.") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 2), @Expect(type = TargetAttributesRequestedEvent.class, count = 1), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) public void testActionHistoryZeroInput() throws Exception { final DistributionSet ds = testdataFactory.createDistributionSet(""); Target savedTarget = testdataFactory.createTarget("911"); @@ -529,13 +529,13 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Test to verify that entire action history is returned if the input value for actionHistory is -1, for getControllerDeploymentActionFeedback endpoint.") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 2), @Expect(type = TargetAttributesRequestedEvent.class, count = 1), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) public void testActionHistoryNegativeInput() throws Exception { final DistributionSet ds = testdataFactory.createDistributionSet(""); Target savedTarget = testdataFactory.createTarget("911"); @@ -654,12 +654,13 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Assign multiple DS in multi-assignment mode. The earliest active Action is exposed to the controller.") public void earliestActionIsExposedToControllerInMultiAssignMode() throws Exception { - setMultiAssignmentsEnabled(); + enableMultiAssignments(); final Target target = testdataFactory.createTarget(); final DistributionSet ds1 = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); final DistributionSet ds2 = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); - final Action action1 = getFirstAssignedAction(assignDistributionSet(ds1, target)); - final Long action2Id = getFirstAssignedActionId(assignDistributionSet(ds2, target)); + final Action action1 = getFirstAssignedAction(assignDistributionSet(ds1.getId(), target.getControllerId(), 56)); + final Long action2Id = getFirstAssignedActionId( + assignDistributionSet(ds2.getId(), target.getControllerId(), 34)); assertDeploymentActionIsExposedToTarget(target.getControllerId(), action1.getId()); sendDeploymentActionFeedback(target, action1, "closed", "success"); @@ -670,7 +671,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("The system should not create a new target because of a too long controller id.") public void rootRsWithInvalidControllerId() throws Exception { - String invalidControllerId = RandomStringUtils.randomAlphabetic(Target.CONTROLLER_ID_MAX_SIZE + 1); + final String invalidControllerId = RandomStringUtils.randomAlphabetic(Target.CONTROLLER_ID_MAX_SIZE + 1); mvc.perform(get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(), invalidControllerId)) .andExpect(status().isBadRequest()); } @@ -684,8 +685,4 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { .andExpect(jsonPath("$._links.deploymentBase.href", containsString(expectedDeploymentBaseLink))); } - - private void setMultiAssignmentsEnabled() { - tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, true); - } } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java index df9977ffc..0941758fc 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java @@ -57,9 +57,12 @@ public class MgmtAction extends MgmtBaseEntity { @JsonProperty private Long forceTime; - @JsonProperty(value="forceType") + @JsonProperty(value = "forceType") private MgmtActionType actionType; + @JsonProperty + private Integer weight; + @JsonProperty private MgmtMaintenanceWindow maintenanceWindow; @@ -79,6 +82,14 @@ public class MgmtAction extends MgmtBaseEntity { this.forceTime = forceTime; } + public Integer getWeight() { + return weight; + } + + public void setWeight(final Integer weight) { + this.weight = weight; + } + public MgmtActionType getActionType() { return actionType; } @@ -110,5 +121,4 @@ public class MgmtAction extends MgmtBaseEntity { public void setType(final String type) { this.type = type; } - } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtTargetAssignmentRequestBody.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtTargetAssignmentRequestBody.java index c9cc2421b..68c8a2fd6 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtTargetAssignmentRequestBody.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtTargetAssignmentRequestBody.java @@ -25,6 +25,7 @@ public class MgmtTargetAssignmentRequestBody { private long forcetime; private MgmtActionType type; private MgmtMaintenanceWindowRequestBody maintenanceWindow; + private Integer weight; /** * JsonCreator Constructor @@ -61,6 +62,14 @@ public class MgmtTargetAssignmentRequestBody { this.forcetime = forcetime; } + public Integer getWeight() { + return weight; + } + + public void setWeight(final Integer weight) { + this.weight = weight; + } + public MgmtMaintenanceWindowRequestBody getMaintenanceWindow() { return maintenanceWindow; } @@ -68,5 +77,4 @@ public class MgmtTargetAssignmentRequestBody { public void setMaintenanceWindow(final MgmtMaintenanceWindowRequestBody maintenanceWindow) { this.maintenanceWindow = maintenanceWindow; } - } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java index 454bfc994..21c41eb3d 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java @@ -12,12 +12,12 @@ import java.util.HashMap; import java.util.Map; import org.eclipse.hawkbit.mgmt.json.model.MgmtNamedEntity; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; /** * @@ -47,6 +47,9 @@ public class MgmtRolloutResponseBody extends MgmtNamedEntity { @JsonProperty private MgmtActionType type; + @JsonProperty + private Integer weight; + public boolean isDeleted() { return deleted; } @@ -114,4 +117,12 @@ public class MgmtRolloutResponseBody extends MgmtNamedEntity { public MgmtActionType getType() { return type; } + + public void setWeight(final Integer weight) { + this.weight = weight; + } + + public Integer getWeight() { + return weight; + } } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java index 9711b31b0..0c98c7561 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java @@ -16,6 +16,7 @@ import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroup; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; /** * Model for request containing a rollout body e.g. in a POST request of @@ -34,6 +35,9 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt private Long startAt; + @JsonProperty(required = false) + private Integer weight; + private MgmtActionType type; private List groups; @@ -124,7 +128,7 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt * @param groups * List of {@link MgmtRolloutGroup} */ - public void setGroups(List groups) { + public void setGroups(final List groups) { this.groups = groups; } @@ -139,7 +143,22 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt * @param startAt * the start at timestamp in millis or null */ - public void setStartAt(Long startAt) { + public void setStartAt(final Long startAt) { this.startAt = startAt; } + + /** + * @return the priority of {@link Rollout} + */ + public Integer getWeight() { + return weight; + } + + /** + * @param weight + * the priority of {@link Rollout} + */ + public void setWeight(final Integer weight) { + this.weight = weight; + } } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignment.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignment.java index 85b0b1a97..fff7b3b07 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignment.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignment.java @@ -17,6 +17,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; public class MgmtDistributionSetAssignment extends MgmtId { private long forcetime; + @JsonProperty(required = false) + private Integer weight; private MgmtActionType type; private MgmtMaintenanceWindowRequestBody maintenanceWindow; @@ -47,6 +49,14 @@ public class MgmtDistributionSetAssignment extends MgmtId { this.forcetime = forcetime; } + public Integer getWeight() { + return weight; + } + + public void setWeight(final int weight) { + this.weight = weight; + } + /** * Returns {@link MgmtMaintenanceWindowRequestBody} for distribution set * assignment. diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targetfilter/MgmtDistributionSetAutoAssignment.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targetfilter/MgmtDistributionSetAutoAssignment.java index ce0d8494c..ce643277c 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targetfilter/MgmtDistributionSetAutoAssignment.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targetfilter/MgmtDistributionSetAutoAssignment.java @@ -22,6 +22,9 @@ public class MgmtDistributionSetAutoAssignment extends MgmtId { @JsonProperty(required = false) private MgmtActionType type; + @JsonProperty(required = false) + private Integer weight; + public MgmtActionType getType() { return type; } @@ -29,4 +32,12 @@ public class MgmtDistributionSetAutoAssignment extends MgmtId { public void setType(final MgmtActionType type) { this.type = type; } + + public Integer getWeight() { + return weight; + } + + public void setWeight(final Integer weight) { + this.weight = weight; + } } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targetfilter/MgmtTargetFilterQuery.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targetfilter/MgmtTargetFilterQuery.java index e32ad2c8b..33077b53f 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targetfilter/MgmtTargetFilterQuery.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targetfilter/MgmtTargetFilterQuery.java @@ -40,6 +40,9 @@ public class MgmtTargetFilterQuery extends MgmtBaseEntity { @JsonProperty private MgmtActionType autoAssignActionType; + @JsonProperty + private Integer autoAssignWeight; + public Long getFilterId() { return filterId; } @@ -80,4 +83,11 @@ public class MgmtTargetFilterQuery extends MgmtBaseEntity { this.autoAssignActionType = actionType; } + public Integer getAutoAssignWeight() { + return autoAssignWeight; + } + + public void setAutoAssignWeight(final Integer autoAssignWeight) { + this.autoAssignWeight = autoAssignWeight; + } } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targetfilter/MgmtTargetFilterQueryRequestBody.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targetfilter/MgmtTargetFilterQueryRequestBody.java index ecfe1291b..c0c25ba88 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targetfilter/MgmtTargetFilterQueryRequestBody.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targetfilter/MgmtTargetFilterQueryRequestBody.java @@ -33,8 +33,7 @@ public class MgmtTargetFilterQueryRequestBody { return query; } - public void setQuery(String query) { + public void setQuery(final String query) { this.query = query; } - } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDeploymentRequestMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDeploymentRequestMapper.java index 73c545a24..108e0e740 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDeploymentRequestMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDeploymentRequestMapper.java @@ -38,7 +38,7 @@ public final class MgmtDeploymentRequestMapper { final String targetId) { return createAssignmentRequest(targetId, dsAssignment.getId(), dsAssignment.getType(), - dsAssignment.getForcetime(), dsAssignment.getMaintenanceWindow()); + dsAssignment.getForcetime(), dsAssignment.getWeight(), dsAssignment.getMaintenanceWindow()); } /** @@ -54,13 +54,14 @@ public final class MgmtDeploymentRequestMapper { final Long dsId) { return createAssignmentRequest(targetAssignment.getId(), dsId, targetAssignment.getType(), - targetAssignment.getForcetime(), targetAssignment.getMaintenanceWindow()); + targetAssignment.getForcetime(), targetAssignment.getWeight(), targetAssignment.getMaintenanceWindow()); } private static DeploymentRequest createAssignmentRequest(final String targetId, final Long dsId, - final MgmtActionType type, final long forcetime, final MgmtMaintenanceWindowRequestBody maintenanceWindow) { + final MgmtActionType type, final long forcetime, final Integer weight, + final MgmtMaintenanceWindowRequestBody maintenanceWindow) { final DeploymentRequestBuilder request = DeploymentManagement.deploymentRequest(targetId, dsId) - .setActionType(MgmtRestModelMapper.convertActionType(type)).setForceTime(forcetime); + .setActionType(MgmtRestModelMapper.convertActionType(type)).setForceTime(forcetime).setWeight(weight); if (maintenanceWindow != null) { final String cronSchedule = maintenanceWindow.getSchedule(); final String duration = maintenanceWindow.getDuration(); @@ -70,5 +71,4 @@ public final class MgmtDeploymentRequestMapper { } return request.build(); } - } 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 f61fd4c36..03d4184d7 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 @@ -266,7 +266,6 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { final List assignmentResults = deployManagament .assignDistributionSets(deploymentRequests); return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(assignmentResults)); - } @Override diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java index ea06d5811..cd4c6cb28 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java @@ -79,6 +79,7 @@ final class MgmtRolloutMapper { body.setTotalTargets(rollout.getTotalTargets()); body.setDeleted(rollout.isDeleted()); body.setType(MgmtRestModelMapper.convertActionType(rollout.getActionType())); + rollout.getWeight().ifPresent(body::setWeight); if (withDetails) { for (final TotalTargetCountStatus.Status status : TotalTargetCountStatus.Status.values()) { @@ -106,7 +107,8 @@ final class MgmtRolloutMapper { return entityFactory.rollout().create().name(restRequest.getName()).description(restRequest.getDescription()) .set(distributionSet).targetFilterQuery(restRequest.getTargetFilterQuery()) .actionType(MgmtRestModelMapper.convertActionType(restRequest.getType())) - .forcedTime(restRequest.getForcetime()).startAt(restRequest.getStartAt()); + .forcedTime(restRequest.getForcetime()).startAt(restRequest.getStartAt()) + .weight(restRequest.getWeight()); } static RolloutGroupCreate fromRequest(final EntityFactory entityFactory, final MgmtRolloutGroup restRequest) { @@ -249,5 +251,4 @@ final class MgmtRolloutMapper { private static String createIllegalArgumentLiteral(final Condition condition) { return "Condition " + condition + NOT_SUPPORTED; } - } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryMapper.java index 0309b8552..935e727d6 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryMapper.java @@ -15,11 +15,14 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtDistributionSetAutoAssignment; import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery; import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQueryRequestBody; import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetFilterQueryRestApi; import org.eclipse.hawkbit.repository.EntityFactory; +import org.eclipse.hawkbit.repository.builder.AutoAssignDistributionSetUpdate; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryCreate; +import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.springframework.util.CollectionUtils; @@ -58,6 +61,7 @@ public final class MgmtTargetFilterQueryMapper { if (distributionSet != null) { targetRest.setAutoAssignDistributionSet(distributionSet.getId()); targetRest.setAutoAssignActionType(MgmtRestModelMapper.convertActionType(filter.getAutoAssignActionType())); + filter.getAutoAssignWeight().ifPresent(targetRest::setAutoAssignWeight); } targetRest.add(linkTo(methodOn(MgmtTargetFilterQueryRestApi.class).getFilter(filter.getId())).withSelfRel()); @@ -76,4 +80,12 @@ public final class MgmtTargetFilterQueryMapper { return entityFactory.targetFilterQuery().create().name(filterRest.getName()).query(filterRest.getQuery()); } + static AutoAssignDistributionSetUpdate fromRequest(final EntityFactory entityFactory, final long filterId, + final MgmtDistributionSetAutoAssignment assignRest) { + final ActionType type = MgmtRestModelMapper.convertActionType(assignRest.getType()); + + return entityFactory.targetFilterQuery().updateAutoAssign(filterId).ds(assignRest.getId()).actionType(type) + .weight(assignRest.getWeight()); + } + } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResource.java index 17006fdcd..082b19fff 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResource.java @@ -108,8 +108,9 @@ public class MgmtTargetFilterQueryResource implements MgmtTargetFilterQueryRestA @RequestBody final MgmtTargetFilterQueryRequestBody targetFilterRest) { LOG.debug("updating target filter query {}", filterId); - final TargetFilterQuery updateFilter = filterManagement.update(entityFactory.targetFilterQuery() - .update(filterId).name(targetFilterRest.getName()).query(targetFilterRest.getQuery())); + final TargetFilterQuery updateFilter = filterManagement + .update(entityFactory.targetFilterQuery().update(filterId).name(targetFilterRest.getName()) + .query(targetFilterRest.getQuery())); final MgmtTargetFilterQuery response = MgmtTargetFilterQueryMapper.toResponse(updateFilter); MgmtTargetFilterQueryMapper.addLinks(response); @@ -127,10 +128,10 @@ public class MgmtTargetFilterQueryResource implements MgmtTargetFilterQueryRestA @Override public ResponseEntity postAssignedDistributionSet( @PathVariable("filterId") final Long filterId, - @RequestBody final MgmtDistributionSetAutoAssignment dsIdWithActionType) { + @RequestBody final MgmtDistributionSetAutoAssignment autoAssignRequest) { - final TargetFilterQuery updateFilter = filterManagement.updateAutoAssignDSWithActionType(filterId, - dsIdWithActionType.getId(), MgmtRestModelMapper.convertActionType(dsIdWithActionType.getType())); + final TargetFilterQuery updateFilter = filterManagement.updateAutoAssignDS( + MgmtTargetFilterQueryMapper.fromRequest(entityFactory, filterId, autoAssignRequest)); final MgmtTargetFilterQuery response = MgmtTargetFilterQueryMapper.toResponse(updateFilter); MgmtTargetFilterQueryMapper.addLinks(response); @@ -156,7 +157,7 @@ public class MgmtTargetFilterQueryResource implements MgmtTargetFilterQueryRestA @Override public ResponseEntity deleteAssignedDistributionSet(@PathVariable("filterId") final Long filterId) { - filterManagement.updateAutoAssignDS(filterId, null); + filterManagement.updateAutoAssignDS(entityFactory.targetFilterQuery().updateAutoAssign(filterId).ds(null)); return ResponseEntity.noContent().build(); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java index edf727b57..8c046c68d 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java @@ -206,6 +206,7 @@ public final class MgmtTargetMapper { if (ActionType.TIMEFORCED == action.getActionType()) { result.setForceTime(action.getForcedTime()); } + action.getWeight().ifPresent(result::setWeight); result.setActionType(MgmtRestModelMapper.convertActionType(action.getActionType())); if (action.isActive()) { @@ -289,5 +290,4 @@ public final class MgmtTargetMapper { static List toResponseTargetMetadata(final List metadata) { return metadata.stream().map(MgmtTargetMapper::toResponseTargetMetadata).collect(Collectors.toList()); } - } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/AbstractManagementApiIntegrationTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/AbstractManagementApiIntegrationTest.java index 91bf72f35..309315707 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/AbstractManagementApiIntegrationTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/AbstractManagementApiIntegrationTest.java @@ -12,6 +12,7 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; import org.eclipse.hawkbit.repository.model.BaseEntity; import org.eclipse.hawkbit.repository.model.NamedEntity; @@ -21,6 +22,8 @@ import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.test.TestConfiguration; import org.eclipse.hawkbit.rest.AbstractRestIntegrationTest; import org.eclipse.hawkbit.rest.RestConfiguration; +import org.json.JSONException; +import org.json.JSONObject; import org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; @@ -197,4 +200,24 @@ public abstract class AbstractManagementApiIntegrationTest extends AbstractRestI }; } + protected static JSONObject getAssignmentObject(final Object id, final MgmtActionType type) { + final JSONObject obj = new JSONObject(); + try { + obj.put("id", id); + obj.put("type", type.getName()); + } catch (final JSONException e) { + e.printStackTrace(); + } + return obj; + } + + protected static JSONObject getAssignmentObject(final Object id, final MgmtActionType type, final int weight) { + try { + return getAssignmentObject(id, type).put("weight", weight); + } catch (final JSONException e) { + e.printStackTrace(); + } + return new JSONObject(); + } + } 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 e8ef170fb..25df64d78 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 @@ -555,9 +555,8 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr final Set createDistributionSetsAlphabetical = createDistributionSetsAlphabetical(1); final DistributionSet createdDs = createDistributionSetsAlphabetical.iterator().next(); - targetFilterQueryManagement.updateAutoAssignDS(targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name(knownFilterName).query("name==y")).getId(), - createdDs.getId()); + targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name(knownFilterName) + .query("name==y").autoAssignDistributionSet(createdDs.getId())); // create some dummy target filter queries targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("b").query("name==y")); @@ -615,12 +614,10 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr private void prepareTestFilters(final String filterNamePrefix, final DistributionSet createdDs) { // create target filter queries that should be found - targetFilterQueryManagement.updateAutoAssignDS(targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "1").query("name==y")) - .getId(), createdDs.getId()); - targetFilterQueryManagement.updateAutoAssignDS(targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "2").query("name==y")) - .getId(), createdDs.getId()); + targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "1") + .query("name==y").autoAssignDistributionSet(createdDs.getId())); + targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "2") + .query("name==y").autoAssignDistributionSet(createdDs.getId())); // create some dummy target filter queries targetFilterQueryManagement .create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "b").query("name==y")); @@ -1247,8 +1244,8 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr } @Test - @Description("Ensures that multi target assignment through API is reflected by the repository in the case of " + - "DOWNLOAD_ONLY.") + @Description("Ensures that multi target assignment through API is reflected by the repository in the case of " + + "DOWNLOAD_ONLY.") public void assignMultipleTargetsToDistributionSetAsDownloadOnly() throws Exception { final DistributionSet createdDs = testdataFactory.createDistributionSet(); @@ -1263,8 +1260,8 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr assignDistributionSet(createdDs.getId(), knownTargetIds[0], Action.ActionType.DOWNLOAD_ONLY); mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", createdDs.getId()) - .contentType(MediaType.APPLICATION_JSON).content(list.toString())) - .andExpect(status().isOk()).andExpect(jsonPath("$.assigned", equalTo(knownTargetIds.length - 1))) + .contentType(MediaType.APPLICATION_JSON).content(list.toString())).andExpect(status().isOk()) + .andExpect(jsonPath("$.assigned", equalTo(knownTargetIds.length - 1))) .andExpect(jsonPath("$.alreadyAssigned", equalTo(1))) .andExpect(jsonPath("$.total", equalTo(knownTargetIds.length))); @@ -1310,10 +1307,10 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr final Long dsId = testdataFactory.createDistributionSet().getId(); final JSONArray body = new JSONArray(); - body.put(getAssignmentObject(targetIds.get(0), MgmtActionType.FORCED)); - body.put(getAssignmentObject(targetIds.get(0), MgmtActionType.FORCED)); - body.put(getAssignmentObject(targetIds.get(1), MgmtActionType.FORCED)); - body.put(getAssignmentObject(targetIds.get(1), MgmtActionType.SOFT)); + body.put(getAssignmentObject(targetIds.get(0), MgmtActionType.FORCED, 56)); + body.put(getAssignmentObject(targetIds.get(0), MgmtActionType.FORCED, 78)); + body.put(getAssignmentObject(targetIds.get(1), MgmtActionType.FORCED, 67)); + body.put(getAssignmentObject(targetIds.get(1), MgmtActionType.SOFT, 34)); enableMultiAssignments(); mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", dsId).content(body.toString()) @@ -1321,14 +1318,32 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr .andExpect(jsonPath("total", equalTo(body.length()))); } + @Test + @Description("An assignment request containing a weight is only accepted when weight is valide and multi assignment is on.") + public void weightValidation() throws Exception { + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + final int weight = 78; + final JSONArray bodyValide = new JSONArray().put(getAssignmentObject(targetId, MgmtActionType.FORCED, weight)); + final JSONArray bodyInvalide = new JSONArray() + .put(getAssignmentObject(targetId, MgmtActionType.FORCED, Action.WEIGHT_MIN - 1)); - public static JSONObject getAssignmentObject(final String targetId, final MgmtActionType type) - throws JSONException { - final JSONObject obj = new JSONObject(); - obj.put("id", targetId); - obj.put("type", type.getName()); - return obj; + mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", dsId).content(bodyValide.toString()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.multiassignmentNotEnabled"))); + enableMultiAssignments(); + mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", dsId).content(bodyInvalide.toString()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.repo.constraintViolation"))); + mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", dsId).content(bodyValide.toString()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + final List actions = deploymentManagement.findActionsAll(PAGE).get().collect(Collectors.toList()); + assertThat(actions).size().isEqualTo(1); + assertThat(actions.get(0).getWeight()).get().isEqualTo(weight); } - } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java index d0ca9eb1e..17f2812a4 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java @@ -916,6 +916,36 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes postRollout("rollout1", 10, dsA.getId(), "id==target*", 20, Action.ActionType.DOWNLOAD_ONLY); } + @Test + @Description("A rollout create request containing a weight is only accepted when weight is valide and multi assignment is on.") + public void weightValidation() throws Exception { + testdataFactory.createTargets(4, "rollout", "description"); + final Long dsId = testdataFactory.createDistributionSet().getId(); + final int weight = 66; + + final String invalideWeightRequest = JsonBuilder.rollout("withWeight", "d", 2, dsId, "id==rollout*", + new RolloutGroupConditionBuilder().withDefaults().build(), null, null, Action.WEIGHT_MIN - 1); + final String valideWeightRequest = JsonBuilder.rollout("withWeight", "d", 2, dsId, "id==rollout*", + new RolloutGroupConditionBuilder().withDefaults().build(), null, null, weight); + + mvc.perform(post("/rest/v1/rollouts").content(valideWeightRequest).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.multiassignmentNotEnabled"))); + enableMultiAssignments(); + mvc.perform(post("/rest/v1/rollouts").content(invalideWeightRequest).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.repo.constraintViolation"))); + mvc.perform(post("/rest/v1/rollouts").content(valideWeightRequest).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isCreated()); + + final List rollouts = rolloutManagement.findAll(PAGE, false).getContent(); + assertThat(rollouts).hasSize(1); + assertThat(rollouts.get(0).getWeight()).get().isEqualTo(weight); + } + protected T doWithTimeout(final Callable callable, final SuccessCondition successCondition, final long timeout, final long pollInterval) throws Exception // NOPMD { @@ -955,14 +985,12 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes private void postRollout(final String name, final int groupSize, final Long distributionSetId, final String targetFilterQuery, final int targets, final Action.ActionType type) throws Exception { - String actionType = MgmtRestModelMapper.convertActionType(type).getName(); - String rollout = JsonBuilder.rollout(name, "desc", groupSize, distributionSetId, targetFilterQuery, - new RolloutGroupConditionBuilder().withDefaults().build(), null, actionType); + final String actionType = MgmtRestModelMapper.convertActionType(type).getName(); + final String rollout = JsonBuilder.rollout(name, "desc", groupSize, distributionSetId, targetFilterQuery, + new RolloutGroupConditionBuilder().withDefaults().build(), null, actionType, null); - mvc.perform(post("/rest/v1/rollouts") - .content(rollout) - .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + mvc.perform(post("/rest/v1/rollouts").content(rollout).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) .andExpect(jsonPath("$.name", equalTo(name))).andExpect(jsonPath("$.status", equalTo("creating"))) .andExpect(jsonPath("$.type", equalTo(actionType))) .andExpect(jsonPath("$.targetFilterQuery", equalTo(targetFilterQuery))) 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 03d232ad7..fe5175f9c 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 @@ -20,12 +20,15 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +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.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; @@ -104,8 +107,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte public void updateTargetWhichDoesNotExistsLeadsToEntityNotFound() throws Exception { final String notExistingId = "4395"; mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + notExistingId).content("{}") - .contentType(MediaType.APPLICATION_JSON)).andDo(print()) - .andExpect(status().isNotFound()); + .contentType(MediaType.APPLICATION_JSON)).andDo(print()).andExpect(status().isNotFound()); } @Test @@ -166,8 +168,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte createSingleTargetFilterQuery(idB, testQuery); createSingleTargetFilterQuery(idC, testQuery); - mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING)).andExpect(status().isOk()) - .andDo(print()) + mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING)).andExpect(status().isOk()).andDo(print()) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(knownTargetAmount))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(knownTargetAmount))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(knownTargetAmount))) @@ -252,9 +253,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte final String hrefPrefix = "http://localhost" + MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId(); // test - mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId())) - .andDo(print()).andExpect(status().isOk()) - .andExpect(jsonPath(JSON_PATH_NAME, equalTo(knownName))) + mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId())).andDo(print()) + .andExpect(status().isOk()).andExpect(jsonPath(JSON_PATH_NAME, equalTo(knownName))) .andExpect(jsonPath(JSON_PATH_QUERY, equalTo(knownQuery))) .andExpect(jsonPath("$._links.self.href", equalTo(hrefPrefix))) .andExpect(jsonPath("$._links.autoAssignDS.href", equalTo(hrefPrefix + "/autoAssignDS"))); @@ -402,8 +402,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte ? "{\"id\":" + set.getId() + ", \"type\":\"" + actionType.getName() + "\"}" : "{\"id\":" + set.getId() + "}"; mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") - .content(payload).contentType(MediaType.APPLICATION_JSON)).andDo(print()) - .andExpect(status().isOk()); + .content(payload).contentType(MediaType.APPLICATION_JSON)).andDo(print()).andExpect(status().isOk()); final TargetFilterQuery updatedFilterQuery = targetFilterQueryManagement.get(tfq.getId()).get(); final MgmtActionType expectedActionType = actionType != null ? actionType : MgmtActionType.FORCED; @@ -412,9 +411,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte assertThat(updatedFilterQuery.getAutoAssignActionType()) .isEqualTo(MgmtRestModelMapper.convertActionType(expectedActionType)); - mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId())) - .andDo(print()).andExpect(status().isOk()) - .andExpect(jsonPath(JSON_PATH_NAME, equalTo(tfq.getName()))) + mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId())).andDo(print()) + .andExpect(status().isOk()).andExpect(jsonPath(JSON_PATH_NAME, equalTo(tfq.getName()))) .andExpect(jsonPath(JSON_PATH_QUERY, equalTo(tfq.getQuery()))) .andExpect(jsonPath(JSON_PATH_AUTO_ASSIGN_DS, equalTo(set.getId().intValue()))) .andExpect(jsonPath(JSON_PATH_AUTO_ASSIGN_ACTION_TYPE, equalTo(expectedActionType.getName()))) @@ -427,8 +425,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte throws Exception { mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") .content("{\"id\":" + set.getId() + ", \"type\":\"" + MgmtActionType.TIMEFORCED.getName() + "\"}") - .contentType(MediaType.APPLICATION_JSON)).andDo(print()) - .andExpect(status().isBadRequest()) + .contentType(MediaType.APPLICATION_JSON)).andDo(print()).andExpect(status().isBadRequest()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(InvalidAutoAssignActionTypeException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, @@ -491,7 +488,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte final DistributionSet set = testdataFactory.createDistributionSet(dsName); final TargetFilterQuery tfq = createSingleTargetFilterQuery(knownName, knownQuery); - targetFilterQueryManagement.updateAutoAssignDS(tfq.getId(), set.getId()); + targetFilterQueryManagement + .updateAutoAssignDS(entityFactory.targetFilterQuery().updateAutoAssign(tfq.getId()).ds(set.getId())); final TargetFilterQuery updatedFilterQuery = targetFilterQueryManagement.get(tfq.getId()).get(); @@ -514,6 +512,34 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte } + @Test + @Description("An auto assignment containing a weight is only accepted when weight is valide and multi assignment is on.") + public void weightValidation() throws Exception { + final Long filterId = createSingleTargetFilterQuery("filter1", "name==*").getId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + + final String invalideWeightRequest = new JSONObject().put("id", dsId).put("weight", Action.WEIGHT_MIN - 1) + .toString(); + final String valideWeightRequest = new JSONObject().put("id", dsId).put("weight", 45).toString(); + + mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/{targetFilterQueryId}/autoAssignDS", + filterId).content(valideWeightRequest).contentType(MediaType.APPLICATION_JSON)).andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.multiassignmentNotEnabled"))); + enableMultiAssignments(); + mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/{targetFilterQueryId}/autoAssignDS", + filterId).content(invalideWeightRequest).contentType(MediaType.APPLICATION_JSON)).andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.repo.constraintViolation"))); + mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/{targetFilterQueryId}/autoAssignDS", + filterId).content(valideWeightRequest).contentType(MediaType.APPLICATION_JSON)).andDo(print()) + .andExpect(status().isOk()); + + final List filters = targetFilterQueryManagement.findAll(PAGE).getContent(); + assertThat(filters).hasSize(1); + assertThat(filters.get(0).getAutoAssignWeight().get()).isEqualTo(45); + } + private TargetFilterQuery createSingleTargetFilterQuery(final String name, final String query) { return targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name(name).query(query)); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java index eafa344ea..6f1e331b3 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java @@ -26,7 +26,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.net.URISyntaxException; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -40,6 +39,7 @@ import javax.validation.ConstraintViolationException; import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.ActionFields; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; @@ -61,7 +61,6 @@ import org.eclipse.hawkbit.rest.util.JsonBuilder; import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; import org.eclipse.hawkbit.util.IpUtil; import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; import org.junit.Test; import org.springframework.data.domain.PageRequest; @@ -1876,7 +1875,8 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest final List dsIds = testdataFactory.createDistributionSets(2).stream().map(DistributionSet::getId) .collect(Collectors.toList()); - final JSONArray body = getAssignmentBody(dsIds); + final JSONArray body = new JSONArray(); + dsIds.forEach(id -> body.put(getAssignmentObject(id, MgmtActionType.FORCED, 67))); mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(body.toString()) .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) @@ -1889,8 +1889,7 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest final String targetId = testdataFactory.createTarget().getControllerId(); final Long dsId = testdataFactory.createDistributionSet().getId(); - final JSONArray body = getAssignmentBody(Collections.singletonList(dsId)); - + final JSONArray body = new JSONArray().put(getAssignmentObject(dsId, MgmtActionType.FORCED)); mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(body.toString()) .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); @@ -1902,7 +1901,8 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest final String targetId = testdataFactory.createTarget().getControllerId(); final Long dsId = testdataFactory.createDistributionSet().getId(); - final JSONArray body = getAssignmentBody(Arrays.asList(dsId, dsId)); + final JSONObject assignment = getAssignmentObject(dsId, MgmtActionType.FORCED); + final JSONArray body = new JSONArray().put(assignment).put(assignment); mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(body.toString()) .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) @@ -1916,21 +1916,90 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest final List dsIds = testdataFactory.createDistributionSets(2).stream().map(DistributionSet::getId) .collect(Collectors.toList()); - final JSONArray body = getAssignmentBody(dsIds); + final JSONArray body = new JSONArray(); + dsIds.forEach(id -> body.put(getAssignmentObject(id, MgmtActionType.FORCED, 76))); enableMultiAssignments(); mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(body.toString()) - .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(2))); + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("total", equalTo(2))); } - public static JSONArray getAssignmentBody(final Collection dsIds) throws JSONException { - final JSONArray body = new JSONArray(); - for (final Long id : dsIds) { - final JSONObject obj = new JSONObject(); - obj.put("id", id); - body.put(obj); - } - return body; + @Test + @Description("An assignment request containing a weight is only accepted when weight is valide and multi assignment is on.") + public void weightValidation() throws Exception { + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + final int weight = 98; + + final JSONObject bodyValid = getAssignmentObject(dsId, MgmtActionType.FORCED, weight); + final JSONObject bodyInvalid = getAssignmentObject(dsId, MgmtActionType.FORCED, Action.WEIGHT_MIN - 1); + + enableMultiAssignments(); + mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(bodyInvalid.toString()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.repo.constraintViolation"))); + mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(bodyValid.toString()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + final List actions = deploymentManagement.findActionsAll(PAGE).get().collect(Collectors.toList()); + assertThat(actions).size().isEqualTo(1); + assertThat(actions.get(0).getWeight()).get().isEqualTo(weight); } + + @Test + @Description("An assignment request containing a valid weight when multi assignment is off.") + public void weightWithSingleAssignment() throws Exception { + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + + final JSONObject bodyValid = getAssignmentObject(dsId, MgmtActionType.FORCED, 98); + + mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(bodyValid.toString()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.multiassignmentNotEnabled"))); + } + + @Test + @Description("An assignment request containing a valid weight when multi assignment is on.") + public void weightWithMultiAssignment() throws Exception { + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + final int weight = 98; + + final JSONObject bodyValid = getAssignmentObject(dsId, MgmtActionType.FORCED, weight); + + enableMultiAssignments(); + mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(bodyValid.toString()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + final List actions = deploymentManagement.findActionsAll(PAGE).get().collect(Collectors.toList()); + assertThat(actions).size().isEqualTo(1); + assertThat(actions.get(0).getWeight()).get().isEqualTo(weight); + } + + @Test + @Description("Get weight of action") + public void getActionWeight() throws Exception { + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + final int customWeightHigh = 800; + final int customWeightLow = 300; + assignDistributionSet(dsId, targetId); + enableMultiAssignments(); + assignDistributionSet(dsId, targetId, customWeightHigh); + assignDistributionSet(dsId, targetId, customWeightLow); + + mvc.perform(get("/rest/v1/targets/{targetId}/actions", targetId) + .param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "WEIGHT:ASC")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("content.[0].weight").doesNotExist()) + .andExpect(jsonPath("content.[1].weight", equalTo(customWeightLow))) + .andExpect(jsonPath("content.[2].weight", equalTo(customWeightHigh))); + + } + } 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 f35381d83..4030a1fd4 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,6 +83,7 @@ public class ResponseExceptionHandler { 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); } private static HttpStatus getStatusOrDefault(final SpServerError error) { diff --git a/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java b/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java index 598f1f9f7..743d492cc 100644 --- a/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java +++ b/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java @@ -449,25 +449,27 @@ public abstract class JsonBuilder { public static String rollout(final String name, final String description, final int groupSize, final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions) { - return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, null, null); + return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, null, null, + null); } public static String rollout(final String name, final String description, final Integer groupSize, final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions, final List groups) { - return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, groups, null); + return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, groups, null, + null); } public static String rollout(final String name, final String description, final Integer groupSize, final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions, final String type) { - return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, null, type); + return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, null, type, + null); } public static String rollout(final String name, final String description, final Integer groupSize, final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions, - final List groups, final String type) { - + final List groups, final String type, final Integer weight) { final JSONObject json = new JSONObject(); try { @@ -481,6 +483,10 @@ public abstract class JsonBuilder { json.put("type", type); } + if (weight != null) { + json.put("weight", weight); + } + if (conditions != null) { final JSONObject successCondition = new JSONObject(); 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 fa2960899..1303cb2b1 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 @@ -25,11 +25,13 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.hawkbit.ddi.rest.resource.DdiApiConfiguration; import org.eclipse.hawkbit.mgmt.rest.resource.MgmtApiConfiguration; +import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ArtifactUpload; +import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; @@ -37,6 +39,7 @@ import org.eclipse.hawkbit.repository.test.TestConfiguration; import org.eclipse.hawkbit.rest.AbstractRestIntegrationTest; import org.eclipse.hawkbit.rest.RestConfiguration; import org.eclipse.hawkbit.rest.util.FilterHttpResponse; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.junit.Before; import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; @@ -98,8 +101,13 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati } public static MyFieldFieldDesc requestFieldWithPath(final String path, final boolean mandatory) { + return requestFieldWithPath(path, mandatory, mandatory ? "X" : ""); + } + + private static MyFieldFieldDesc requestFieldWithPath(final String path, final boolean mandatory, + final String mandatoryMessage) { final MyFieldFieldDesc myFieldDesc = new MyFieldFieldDesc(path); - myFieldDesc.attributes(key("mandatory").value(mandatory ? "X" : "")); + myFieldDesc.attributes(key("mandatory").value(mandatoryMessage)); // defaults myFieldDesc.attributes(key("value").value("")); @@ -118,6 +126,10 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati return requestFieldWithPath(path, false); } + public static MyFieldFieldDesc requestFieldWithPathMandatoryInMultiAssignMode(final String path) { + return requestFieldWithPath(path, false, "when multi-assignment is enabled"); + } + public static class MyFieldFieldDesc extends SubsectionDescriptor { /** @@ -154,11 +166,17 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati 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 = maintenanceWindowSchedule == null - ? assignWithoutMaintenanceWindow(distributionSet, savedTarget, timeforced) - : assignWithMaintenanceWindow(distributionSet, savedTarget, timeforced, maintenanceWindowSchedule, - maintenanceWindowDuration, maintenanceWindowTimeZone); + final DeploymentRequestBuilder deploymentRequestBuilder = DeploymentManagement + .deploymentRequest(savedTarget.getControllerId(), distributionSet.getId()) + .setMaintenance(maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone); + if (timeforced) { + deploymentRequestBuilder.setActionType(ActionType.TIMEFORCED); + } + if (isMultiAssignmentsEnabled()) { + deploymentRequestBuilder.setWeight(600); + } + final List updatedTargets = makeAssignment(deploymentRequestBuilder.build()).getAssignedEntity() + .stream().map(Action::getTarget).collect(Collectors.toList()); if (inSync) { feedbackToByInSync(distributionSet); @@ -167,28 +185,6 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati return updatedTargets.get(0); } - private List assignWithoutMaintenanceWindow(final DistributionSet distributionSet, final Target savedTarget, - final boolean timeforced) { - final List actions = timeforced - ? assignDistributionSet(distributionSet.getId(), savedTarget.getControllerId(), ActionType.TIMEFORCED) - .getAssignedEntity() - : assignDistributionSet(distributionSet, savedTarget).getAssignedEntity(); - return actions.stream().map(Action::getTarget).collect(Collectors.toList()); - } - - private List assignWithMaintenanceWindow(final DistributionSet distributionSet, final Target savedTarget, - final boolean timeforced, final String maintenanceWindowSchedule, final String maintenanceWindowDuration, - final String maintenanceWindowTimeZone) { - final List actions = timeforced - ? assignDistributionSetWithMaintenanceWindow(distributionSet.getId(), savedTarget.getControllerId(), - ActionType.TIMEFORCED, maintenanceWindowSchedule, maintenanceWindowDuration, - maintenanceWindowTimeZone).getAssignedEntity() - : assignDistributionSetWithMaintenanceWindow(distributionSet.getId(), savedTarget.getControllerId(), - maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone) - .getAssignedEntity(); - return actions.stream().map(Action::getTarget).collect(Collectors.toList()); - } - protected DistributionSet createDistributionSet() { DistributionSet distributionSet = testdataFactory.createDistributionSet(""); distributionSet = distributionSetManagement.update(entityFactory.distributionSet() @@ -314,4 +310,9 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati parameterWithName("q").description(ApiModelPropertiesGeneric.FIQL)); } + protected boolean isMultiAssignmentsEnabled() { + return Boolean.TRUE.equals(tenantConfigurationManagement + .getConfigurationValue(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, Boolean.class).getValue()); + } + } 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 15a7f3b8b..33339dc53 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 @@ -109,6 +109,8 @@ public final class MgmtApiModelProperties { public static final String ROLLOUT_LINKS_GROUPS = "Link to retrieve the groups a rollout"; public static final String ROLLOUT_START_ASYNC = "Start the rollout asynchronous"; + public static final String RESULTING_ACTIONS_WEIGHT = "Weight of the resulting Actions"; + public static final String UPDATE_STATUS = "Current update status of the target."; public static final String TARGET_ATTRIBUTES = "Target attributes."; @@ -138,6 +140,8 @@ public final class MgmtApiModelProperties { public static final String ACTION_LIST = "List of actions."; + public static final String ACTION_WEIGHT = "Weight of the action showing the importance of the update."; + public static final String IP_ADDRESS = "Last known IP address of the target. Only presented if IP address of the target itself is known (connected directly through DDI API)."; public static final String ADDRESS = "The last known address URI of the target. Includes information of the target is connected either directly (DDI) through HTTP or indirectly (DMF) through amqp."; @@ -188,6 +192,7 @@ public final class MgmtApiModelProperties { // request parameter public static final String FORCETIME = "Forcetime in milliseconds."; public static final String FORCE = "Force as boolean."; + public static final String ASSIGNMENT_WEIGHT = "Importance of the assignment."; public static final String ASSIGNMENT_TYPE = "The type of the assignment."; public static final String TARGET_ASSIGNED = "The number of targets that have been assigned as part of this operation."; public static final String TARGET_ASSIGNED_ALREADY = "The number of targets which already had been the assignment."; 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 e793f395a..97f50126a 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 @@ -385,6 +385,9 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat .description(MgmtApiModelProperties.OFFLINE_UPDATE).optional()), requestFields( requestFieldWithPath("[].id").description(ApiModelPropertiesGeneric.ITEM_ID), + requestFieldWithPathMandatoryInMultiAssignMode("[].weight") + .description(MgmtApiModelProperties.ASSIGNMENT_WEIGHT) + .type(JsonFieldType.NUMBER).attributes(key("value").value("0 - 1000")), optionalRequestFieldWithPath("[].forcetime") .description(MgmtApiModelProperties.FORCETIME), optionalRequestFieldWithPath("[].maintenanceWindow") diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java index 329225cb5..ab4167224 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java @@ -23,10 +23,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import com.fasterxml.jackson.core.JsonProcessingException; - import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; +import org.eclipse.hawkbit.repository.builder.RolloutCreate; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupErrorAction; @@ -52,6 +51,8 @@ import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.snippet.Snippet; +import com.fasterxml.jackson.core.JsonProcessingException; + import io.qameta.allure.Description; import io.qameta.allure.Feature; import io.qameta.allure.Story; @@ -80,7 +81,7 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati @Description("Handles the GET request of retrieving all rollouts. Required Permission: " + SpPermission.READ_ROLLOUT) public void getRollouts() throws Exception { - + enableMultiAssignments(); createRolloutEntity(); mockMvc.perform(get(MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING).accept(MediaTypes.HAL_JSON_VALUE)) @@ -113,6 +114,8 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati allFieldDescriptor.add(fieldWithPath(arrayPrefix + "id").description(ApiModelPropertiesGeneric.ITEM_ID)); allFieldDescriptor.add(fieldWithPath(arrayPrefix + "name").description(ApiModelPropertiesGeneric.NAME)); + allFieldDescriptor.add(fieldWithPath(arrayPrefix + "weight") + .description(MgmtApiModelProperties.RESULTING_ACTIONS_WEIGHT).type(JsonFieldType.NUMBER).optional()); allFieldDescriptor.add(fieldWithPath(arrayPrefix + "deleted").description(ApiModelPropertiesGeneric.DELETED)); allFieldDescriptor .add(fieldWithPath(arrayPrefix + "description").description(ApiModelPropertiesGeneric.DESCRPTION)); @@ -159,7 +162,7 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati @Description("Handles the GET request of retrieving a single rollout. Required Permission: " + SpPermission.READ_ROLLOUT) public void getRollout() throws Exception { - + enableMultiAssignments(); final Rollout rollout = createRolloutEntity(); mockMvc.perform(get(MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING + "/{rolloutId}", rollout.getId()) @@ -195,47 +198,47 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati .contentType(MediaTypes.HAL_JSON_UTF8).accept(MediaTypes.HAL_JSON_VALUE)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) .andExpect(content().contentType(MediaTypes.HAL_JSON_UTF8)) - .andDo(this.document.document( - requestFields(requestFieldWithPath("name").description(ApiModelPropertiesGeneric.NAME), - optionalRequestFieldWithPath("type").description( - MgmtApiModelProperties.ROLLOUT_TYPE).attributes( - key("value").value("['soft', 'forced', 'timeforced', 'downloadonly']")), - requestFieldWithPath("distributionSetId") - .description(MgmtApiModelProperties.ROLLOUT_DS_ID), - requestFieldWithPath("targetFilterQuery") - .description(MgmtApiModelProperties.ROLLOUT_FILTER_QUERY), - requestFieldWithPath("amountGroups") - .description(MgmtApiModelProperties.ROLLOUT_AMOUNT_GROUPS), - optionalRequestFieldWithPath("description") - .description(ApiModelPropertiesGeneric.DESCRPTION), - optionalRequestFieldWithPath("successCondition") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION), - optionalRequestFieldWithPath("successCondition.condition") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION_CONDITION) - .attributes(key("value").value("['threshold']")), - optionalRequestFieldWithPath("successCondition.expression") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION_EXP), - optionalRequestFieldWithPath("successAction") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION), - optionalRequestFieldWithPath("successAction.action") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION_ACTION) - .attributes(key("value").value("['nextgroup']")), - optionalRequestFieldWithPath("successAction.expression") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION_EXP), - optionalRequestFieldWithPath("errorCondition") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION), - optionalRequestFieldWithPath("errorCondition.condition") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION_CONDITION) - .attributes(key("value").value("['threshold']")), - optionalRequestFieldWithPath("errorCondition.expression") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION_EXP), - optionalRequestFieldWithPath("errorAction") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION), - optionalRequestFieldWithPath("errorAction.action") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION_ACTION) - .attributes(key("value").value("['pause']")), - optionalRequestFieldWithPath("errorAction.expression") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION_EXP)), + .andDo(this.document.document(requestFields( + requestFieldWithPath("name").description(ApiModelPropertiesGeneric.NAME), + requestFieldWithPathMandatoryInMultiAssignMode("weight").type(JsonFieldType.NUMBER) + .description(MgmtApiModelProperties.RESULTING_ACTIONS_WEIGHT) + .attributes(key("value").value("0 - 1000")), + requestFieldWithPath("name").description(ApiModelPropertiesGeneric.NAME), + optionalRequestFieldWithPath("type").description(MgmtApiModelProperties.ROLLOUT_TYPE) + .attributes(key("value").value("['soft', 'forced', 'timeforced', 'downloadonly']")), + requestFieldWithPath("distributionSetId").description(MgmtApiModelProperties.ROLLOUT_DS_ID), + requestFieldWithPath("targetFilterQuery") + .description(MgmtApiModelProperties.ROLLOUT_FILTER_QUERY), + requestFieldWithPath("amountGroups").description(MgmtApiModelProperties.ROLLOUT_AMOUNT_GROUPS), + optionalRequestFieldWithPath("description").description(ApiModelPropertiesGeneric.DESCRPTION), + optionalRequestFieldWithPath("successCondition") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION), + optionalRequestFieldWithPath("successCondition.condition") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION_CONDITION) + .attributes(key("value").value("['threshold']")), + optionalRequestFieldWithPath("successCondition.expression") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION_EXP), + optionalRequestFieldWithPath("successAction") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION), + optionalRequestFieldWithPath("successAction.action") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION_ACTION) + .attributes(key("value").value("['nextgroup']")), + optionalRequestFieldWithPath("successAction.expression") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION_EXP), + optionalRequestFieldWithPath("errorCondition") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION), + optionalRequestFieldWithPath("errorCondition.condition") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION_CONDITION) + .attributes(key("value").value("['threshold']")), + optionalRequestFieldWithPath("errorCondition.expression") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION_EXP), + optionalRequestFieldWithPath("errorAction") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION), + optionalRequestFieldWithPath("errorAction.action") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION_ACTION) + .attributes(key("value").value("['pause']")), + optionalRequestFieldWithPath("errorAction.expression") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION_EXP)), getRolloutResponseFields(false, true))); @@ -283,77 +286,83 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati .contentType(MediaType.APPLICATION_JSON).accept(MediaTypes.HAL_JSON_VALUE)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) .andExpect(content().contentType(MediaTypes.HAL_JSON_UTF8)) - .andDo(this.document.document(requestFields( - requestFieldWithPath("name").description(ApiModelPropertiesGeneric.NAME), - requestFieldWithPath("distributionSetId").description(MgmtApiModelProperties.ROLLOUT_DS_ID), - requestFieldWithPath("targetFilterQuery") - .description(MgmtApiModelProperties.ROLLOUT_FILTER_QUERY), - optionalRequestFieldWithPath("description").description(ApiModelPropertiesGeneric.DESCRPTION), - optionalRequestFieldWithPath("successCondition") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION), - optionalRequestFieldWithPath("successCondition.condition") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION_CONDITION) - .attributes(key("value").value("['threshold']")), - optionalRequestFieldWithPath("successCondition.expression") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION_EXP), - optionalRequestFieldWithPath("successAction") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION), - optionalRequestFieldWithPath("successAction.action") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION_ACTION) - .attributes(key("value").value("['nextgroup']")), - optionalRequestFieldWithPath("successAction.expression") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION_EXP), - optionalRequestFieldWithPath("errorCondition") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION), - optionalRequestFieldWithPath("errorCondition.condition") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION_CONDITION) - .attributes(key("value").value("['threshold']")), - optionalRequestFieldWithPath("errorCondition.expression") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION_EXP), - optionalRequestFieldWithPath("errorAction") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION), - optionalRequestFieldWithPath("errorAction.action") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION_ACTION) - .attributes(key("value").value("['pause']")), - optionalRequestFieldWithPath("errorAction.expression") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION_EXP), - - requestFieldWithPath("groups").description(MgmtApiModelProperties.ROLLOUT_GROUPS), - requestFieldWithPath("groups[].name").description(ApiModelPropertiesGeneric.NAME), - requestFieldWithPath("groups[].description").description(ApiModelPropertiesGeneric.DESCRPTION), - optionalRequestFieldWithPath("groups[].targetFilterQuery") - .description(MgmtApiModelProperties.ROLLOUT_GROUP_FILTER_QUERY), - optionalRequestFieldWithPath("groups[].targetPercentage") - .description(MgmtApiModelProperties.ROLLOUT_GROUP_TARGET_PERCENTAGE) - .attributes(key("value").value("0..100")), - optionalRequestFieldWithPath("groups[].successCondition") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION), - optionalRequestFieldWithPath("groups[].successCondition.condition") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION_CONDITION) - .attributes(key("value").value("['threshold']")), - optionalRequestFieldWithPath("groups[].successCondition.expression") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION_EXP), - optionalRequestFieldWithPath("groups[].successAction") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION), - optionalRequestFieldWithPath("groups[].successAction.action") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION_ACTION) - .attributes(key("value").value("['nextgroup']")), - optionalRequestFieldWithPath("groups[].successAction.expression") - .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION_EXP), - optionalRequestFieldWithPath("groups[].errorCondition") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION), - optionalRequestFieldWithPath("groups[].errorCondition.condition") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION_CONDITION) - .attributes(key("value").value("['threshold']")), - optionalRequestFieldWithPath("groups[].errorCondition.expression") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION_EXP), - optionalRequestFieldWithPath("groups[].errorAction") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION), - optionalRequestFieldWithPath("groups[].errorAction.action") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION_ACTION) - .attributes(key("value").value("['pause']")), - optionalRequestFieldWithPath("groups[].errorAction.expression") - .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION_EXP)), + .andDo(this.document.document( + requestFields(requestFieldWithPath("name").description(ApiModelPropertiesGeneric.NAME), + requestFieldWithPathMandatoryInMultiAssignMode("weight") + .type(JsonFieldType.NUMBER) + .description(MgmtApiModelProperties.RESULTING_ACTIONS_WEIGHT) + .attributes(key("value").value("0 - 1000")), + requestFieldWithPath("distributionSetId") + .description(MgmtApiModelProperties.ROLLOUT_DS_ID), + requestFieldWithPath("targetFilterQuery") + .description(MgmtApiModelProperties.ROLLOUT_FILTER_QUERY), + optionalRequestFieldWithPath("description") + .description(ApiModelPropertiesGeneric.DESCRPTION), + optionalRequestFieldWithPath("successCondition") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION), + optionalRequestFieldWithPath("successCondition.condition") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION_CONDITION) + .attributes(key("value").value("['threshold']")), + optionalRequestFieldWithPath("successCondition.expression") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION_EXP), + optionalRequestFieldWithPath("successAction") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION), + optionalRequestFieldWithPath("successAction.action") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION_ACTION) + .attributes(key("value").value("['nextgroup']")), + optionalRequestFieldWithPath("successAction.expression") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION_EXP), + optionalRequestFieldWithPath("errorCondition") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION), + optionalRequestFieldWithPath("errorCondition.condition") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION_CONDITION) + .attributes(key("value").value("['threshold']")), + optionalRequestFieldWithPath("errorCondition.expression") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION_EXP), + optionalRequestFieldWithPath("errorAction") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION), + optionalRequestFieldWithPath("errorAction.action") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION_ACTION) + .attributes(key("value").value("['pause']")), + optionalRequestFieldWithPath("errorAction.expression") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION_EXP), + requestFieldWithPath("groups").description(MgmtApiModelProperties.ROLLOUT_GROUPS), + requestFieldWithPath("groups[].name").description(ApiModelPropertiesGeneric.NAME), + requestFieldWithPath("groups[].description") + .description(ApiModelPropertiesGeneric.DESCRPTION), + optionalRequestFieldWithPath("groups[].targetFilterQuery") + .description(MgmtApiModelProperties.ROLLOUT_GROUP_FILTER_QUERY), + optionalRequestFieldWithPath("groups[].targetPercentage") + .description(MgmtApiModelProperties.ROLLOUT_GROUP_TARGET_PERCENTAGE) + .attributes(key("value").value("0..100")), + optionalRequestFieldWithPath("groups[].successCondition") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION), + optionalRequestFieldWithPath("groups[].successCondition.condition") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION_CONDITION) + .attributes(key("value").value("['threshold']")), + optionalRequestFieldWithPath("groups[].successCondition.expression") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_CONDITION_EXP), + optionalRequestFieldWithPath("groups[].successAction") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION), + optionalRequestFieldWithPath("groups[].successAction.action") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION_ACTION) + .attributes(key("value").value("['nextgroup']")), + optionalRequestFieldWithPath("groups[].successAction.expression") + .description(MgmtApiModelProperties.ROLLOUT_SUCCESS_ACTION_EXP), + optionalRequestFieldWithPath("groups[].errorCondition") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION), + optionalRequestFieldWithPath("groups[].errorCondition.condition") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION_CONDITION) + .attributes(key("value").value("['threshold']")), + optionalRequestFieldWithPath("groups[].errorCondition.expression") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_CONDITION_EXP), + optionalRequestFieldWithPath("groups[].errorAction") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION), + optionalRequestFieldWithPath("groups[].errorAction.action") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION_ACTION) + .attributes(key("value").value("['pause']")), + optionalRequestFieldWithPath("groups[].errorAction.expression") + .description(MgmtApiModelProperties.ROLLOUT_ERROR_ACTION_EXP)), getRolloutResponseFields(false, true))); } @@ -626,11 +635,13 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati private Rollout createRolloutEntity() { testdataFactory.createTargets(20, "exampleTarget"); - final Rollout rollout = rolloutManagement.create( - entityFactory.rollout().create().name("exampleRollout") - .targetFilterQuery("controllerId==exampleTarget*").set(testdataFactory.createDistributionSet()), - 10, new RolloutGroupConditionBuilder().withDefaults() - .successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build()); + final RolloutCreate rolloutCreate = entityFactory.rollout().create().name("exampleRollout") + .targetFilterQuery("controllerId==exampleTarget*").set(testdataFactory.createDistributionSet()); + if (isMultiAssignmentsEnabled()) { + rolloutCreate.weight(400); + } + final Rollout rollout = rolloutManagement.create(rolloutCreate, 10, new RolloutGroupConditionBuilder() + .withDefaults().successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build()); // Run here, because Scheduler is disabled during tests rolloutManagement.handleRollouts(); diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetFilterQueriesResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetFilterQueriesResourceDocumentationTest.java index 85b846fe6..e1f130abb 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetFilterQueriesResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetFilterQueriesResourceDocumentationTest.java @@ -31,6 +31,7 @@ import org.eclipse.hawkbit.rest.documentation.AbstractApiRestDocumentation; import org.eclipse.hawkbit.rest.documentation.ApiModelPropertiesGeneric; import org.eclipse.hawkbit.rest.documentation.MgmtApiModelProperties; import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; +import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.springframework.http.MediaType; @@ -183,15 +184,16 @@ public class TargetFilterQueriesResourceDocumentationTest extends AbstractApiRes @Test @Description("Handles the POST request of setting a distribution set for auto assignment within SP. Required Permission: CREATE_TARGET.") public void postAutoAssignDS() throws Exception { + enableMultiAssignments(); final TargetFilterQuery tfq = createTargetFilterQuery(); final DistributionSet distributionSet = createDistributionSet(); - final String filterByDistSet = "{\"id\":\"" + distributionSet.getId() + "\", \"type\":\"" - + MgmtActionType.SOFT.getName() + "\"}"; + final String autoAssignBody = new JSONObject().put("id", distributionSet.getId()) + .put("type", MgmtActionType.SOFT.getName()).put("weight", 200).toString(); this.mockMvc .perform( post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/{targetFilterQueryId}/autoAssignDS", - tfq.getId()).contentType(MediaType.APPLICATION_JSON).content(filterByDistSet)) + tfq.getId()).contentType(MediaType.APPLICATION_JSON).content(autoAssignBody.toString())) .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) .andDo(this.document.document( pathParameters(parameterWithName("targetFilterQueryId") @@ -199,7 +201,10 @@ public class TargetFilterQueriesResourceDocumentationTest extends AbstractApiRes requestFields(requestFieldWithPath("id").description(MgmtApiModelProperties.DS_ID), optionalRequestFieldWithPath("type") .description(MgmtApiModelProperties.ACTION_FORCE_TYPE) - .attributes(key("value").value("['forced', 'soft', 'downloadonly']"))), + .attributes(key("value").value("['forced', 'soft', 'downloadonly']")), + requestFieldWithPathMandatoryInMultiAssignMode("weight") + .description(MgmtApiModelProperties.RESULTING_ACTIONS_WEIGHT) + .attributes(key("value").value("0 - 1000"))), getResponseFieldTargetFilterQuery(false))); } @@ -227,6 +232,9 @@ public class TargetFilterQueriesResourceDocumentationTest extends AbstractApiRes fieldWithPath(arrayPrefix + "autoAssignActionType") .description(MgmtApiModelProperties.ACTION_FORCE_TYPE).type(JsonFieldType.STRING.toString()) .attributes(key("value").value("['forced', 'soft', 'downloadonly']")), + fieldWithPath(arrayPrefix + "autoAssignWeight") + .description(MgmtApiModelProperties.RESULTING_ACTIONS_WEIGHT) + .type(JsonFieldType.NUMBER.toString()), fieldWithPath(arrayPrefix + "createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), fieldWithPath(arrayPrefix + "createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), fieldWithPath(arrayPrefix + "lastModifiedAt").description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT), @@ -235,7 +243,8 @@ public class TargetFilterQueriesResourceDocumentationTest extends AbstractApiRes .description(MgmtApiModelProperties.TARGET_FILTER_QUERY_LINK_AUTO_ASSIGN_DS)); } - private String createTargetFilterQueryJson(final String name, final String query) throws JsonProcessingException { + private String createTargetFilterQueryJson(final String name, final String query) + throws JsonProcessingException { final Map target = new HashMap<>(); target.put("name", name); target.put("query", query); @@ -249,6 +258,7 @@ public class TargetFilterQueriesResourceDocumentationTest extends AbstractApiRes private TargetFilterQuery createTargetFilterQueryWithDS(final DistributionSet distributionSet) { final TargetFilterQuery targetFilterQuery = createTargetFilterQuery(); - return targetFilterQueryManagement.updateAutoAssignDS(targetFilterQuery.getId(), distributionSet.getId()); + return targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() + .updateAutoAssign(targetFilterQuery.getId()).ds(distributionSet.getId())); } } diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java index d9da9d456..189b801ad 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java @@ -203,6 +203,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Handles the GET request of retrieving the full action history of a specific target. Required Permission: READ_TARGET.") public void getActionsFromTarget() throws Exception { + enableMultiAssignments(); generateActionForTarget(targetId); mockMvc.perform( @@ -229,12 +230,14 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio .description(MgmtApiModelProperties.ACTION_EXECUTION_STATUS) .attributes(key("value").value("['finished', 'pending']")), fieldWithPath("content[]._links").description(MgmtApiModelProperties.LINK_TO_ACTION), - fieldWithPath("content[].id").description(MgmtApiModelProperties.ACTION_ID)))); + fieldWithPath("content[].id").description(MgmtApiModelProperties.ACTION_ID), + fieldWithPath("content[].weight").description(MgmtApiModelProperties.ACTION_WEIGHT)))); } @Test @Description("Handles the GET request of retrieving the full action history of a specific target with maintenance window. Required Permission: READ_TARGET.") public void getActionsFromTargetWithMaintenanceWindow() throws Exception { + enableMultiAssignments(); generateActionForTarget(targetId, true, false, getTestSchedule(2), getTestDuration(1), getTestTimeZone()); mockMvc.perform( @@ -262,6 +265,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio .attributes(key("value").value("['finished', 'pending']")), fieldWithPath("content[]._links").description(MgmtApiModelProperties.LINK_TO_ACTION), fieldWithPath("content[].id").description(MgmtApiModelProperties.ACTION_ID), + fieldWithPath("content[].weight").description(MgmtApiModelProperties.ACTION_WEIGHT), fieldWithPath("content[].maintenanceWindow") .description(MgmtApiModelProperties.MAINTENANCE_WINDOW), fieldWithPath("content[].maintenanceWindow.schedule") @@ -317,6 +321,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Handles the GET request of retrieving a specific action on a specific target. Required Permission: READ_TARGET.") public void getActionFromTarget() throws Exception { + enableMultiAssignments(); final Action action = generateActionForTarget(targetId, true, true); assertThat(deploymentManagement.findAction(action.getId()).get().getActionType()) .isEqualTo(ActionType.TIMEFORCED); @@ -330,6 +335,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio responseFields(fieldWithPath("createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), fieldWithPath("createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), fieldWithPath("id").description(MgmtApiModelProperties.ACTION_ID), + fieldWithPath("weight").description(MgmtApiModelProperties.ACTION_WEIGHT), fieldWithPath("lastModifiedBy").description(ApiModelPropertiesGeneric.LAST_MODIFIED_BY) .type("String"), fieldWithPath("lastModifiedAt").description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT) @@ -351,6 +357,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Handles the GET request of retrieving a specific action on a specific target. Required Permission: READ_TARGET.") public void getActionFromTargetWithMaintenanceWindow() throws Exception { + enableMultiAssignments(); final Action action = generateActionForTarget(targetId, true, true, getTestSchedule(2), getTestDuration(1), getTestTimeZone()); @@ -363,6 +370,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio responseFields(fieldWithPath("createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), fieldWithPath("createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), fieldWithPath("id").description(MgmtApiModelProperties.ACTION_ID), + fieldWithPath("weight").description(MgmtApiModelProperties.ACTION_WEIGHT), fieldWithPath("lastModifiedBy").description(ApiModelPropertiesGeneric.LAST_MODIFIED_BY) .type("String"), fieldWithPath("lastModifiedAt").description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT) @@ -514,6 +522,9 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio .description(MgmtApiModelProperties.OFFLINE_UPDATE).optional()), requestFields( requestFieldWithPath("id").description(ApiModelPropertiesGeneric.ITEM_ID), + requestFieldWithPathMandatoryInMultiAssignMode("weight") + .description(MgmtApiModelProperties.ASSIGNMENT_WEIGHT) + .type(JsonFieldType.NUMBER).attributes(key("value").value("0 - 1000")), optionalRequestFieldWithPath("forcetime").description(MgmtApiModelProperties.FORCETIME), optionalRequestFieldWithPath("maintenanceWindow") .description(MgmtApiModelProperties.MAINTENANCE_WINDOW), @@ -537,12 +548,12 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio final long forceTime = System.currentTimeMillis(); final JSONArray body = new JSONArray(); - body.put(new JSONObject().put("id", sets.get(1).getId()).put("type", "timeforced") + body.put(new JSONObject().put("id", sets.get(1).getId()).put("weight", 500).put("type", "timeforced") .put("forcetime", forceTime) .put("maintenanceWindow", new JSONObject().put("schedule", getTestSchedule(100)) .put("duration", getTestDuration(10)).put("timezone", getTestTimeZone()))) .toString(); - body.put(new JSONObject().put("id", sets.get(0).getId()).put("type", "forced")); + body.put(new JSONObject().put("id", sets.get(0).getId()).put("type", "forced").put("weight", 800)); enableMultiAssignments(); mockMvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" @@ -555,6 +566,9 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio .description(MgmtApiModelProperties.OFFLINE_UPDATE).optional()), requestFields( requestFieldWithPath("[].id").description(ApiModelPropertiesGeneric.ITEM_ID), + requestFieldWithPathMandatoryInMultiAssignMode("[].weight") + .description(MgmtApiModelProperties.ASSIGNMENT_WEIGHT) + .attributes(key("value").value("0 - 1000")), optionalRequestFieldWithPath("[].forcetime") .description(MgmtApiModelProperties.FORCETIME), optionalRequestFieldWithPath("[].maintenanceWindow") diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/DistributionSetSelectWindow.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/DistributionSetSelectWindow.java index ad3a9c59f..23fb6726a 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/DistributionSetSelectWindow.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/DistributionSetSelectWindow.java @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.ui.filtermanagement; import java.util.function.Consumer; +import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; @@ -54,6 +55,7 @@ public class DistributionSetSelectWindow implements CommonDialogWindow.SaveDialo private final EventBus.UIEventBus eventBus; private final TargetManagement targetManagement; private final TargetFilterQueryManagement targetFilterQueryManagement; + private final EntityFactory entityFactory; private CheckBox checkBox; private ActionTypeOptionGroupAutoAssignmentLayout actionTypeOptionGroupLayout; @@ -62,12 +64,13 @@ public class DistributionSetSelectWindow implements CommonDialogWindow.SaveDialo DistributionSetSelectWindow(final VaadinMessageSource i18n, final UIEventBus eventBus, final UINotification notification, final TargetManagement targetManagement, - final TargetFilterQueryManagement targetFilterQueryManagement) { + final TargetFilterQueryManagement targetFilterQueryManagement, final EntityFactory entityFactory) { this.i18n = i18n; this.notification = notification; this.eventBus = eventBus; this.targetManagement = targetManagement; this.targetFilterQueryManagement = targetFilterQueryManagement; + this.entityFactory = entityFactory; } private VerticalLayout initView() { @@ -197,7 +200,8 @@ public class DistributionSetSelectWindow implements CommonDialogWindow.SaveDialo if (dsId != null) { confirmWithConsequencesDialog(tfq, dsId, actionType); } else { - targetFilterQueryManagement.updateAutoAssignDS(targetFilterQueryId, null); + targetFilterQueryManagement.updateAutoAssignDS( + entityFactory.targetFilterQuery().updateAutoAssign(targetFilterQueryId).ds(null)); eventBus.publish(this, CustomFilterUIEvent.UPDATED_TARGET_FILTER_QUERY); } } @@ -206,7 +210,8 @@ public class DistributionSetSelectWindow implements CommonDialogWindow.SaveDialo final ActionType actionType) { final ConfirmConsequencesDialog dialog = new ConfirmConsequencesDialog(tfq, dsId, accepted -> { if (accepted) { - targetFilterQueryManagement.updateAutoAssignDSWithActionType(tfq.getId(), dsId, actionType); + targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() + .updateAutoAssign(tfq.getId()).ds(dsId).actionType(actionType)); eventBus.publish(this, CustomFilterUIEvent.UPDATED_TARGET_FILTER_QUERY); } }); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/FilterManagementView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/FilterManagementView.java index 0c1a000ca..0ea2cbbf9 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/FilterManagementView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/FilterManagementView.java @@ -70,7 +70,7 @@ public class FilterManagementView extends VerticalLayout implements View { final AutoCompleteTextFieldComponent queryTextField, final TargetManagement targetManagement) { this.targetFilterHeader = new TargetFilterHeader(eventBus, filterManagementUIState, permissionChecker, i18n); this.targetFilterTable = new TargetFilterTable(i18n, notification, eventBus, filterManagementUIState, - targetFilterQueryManagement, targetManagement, permissionChecker); + targetFilterQueryManagement, targetManagement, permissionChecker, entityFactory); this.createNewFilterHeader = new CreateOrUpdateFilterHeader(i18n, eventBus, filterManagementUIState, targetFilterQueryManagement, permissionChecker, notification, uiProperties, entityFactory, queryTextField); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java index 1c2e27298..06386ef5a 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Map; import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.model.Action.ActionType; @@ -80,7 +81,7 @@ public class TargetFilterTable extends Table { public TargetFilterTable(final VaadinMessageSource i18n, final UINotification notification, final UIEventBus eventBus, final FilterManagementUIState filterManagementUIState, final TargetFilterQueryManagement targetFilterQueryManagement, final TargetManagement targetManagement, - final SpPermissionChecker permChecker) { + final SpPermissionChecker permChecker, final EntityFactory entityFactory) { this.i18n = i18n; this.notification = notification; this.eventBus = eventBus; @@ -89,7 +90,7 @@ public class TargetFilterTable extends Table { this.permChecker = permChecker; this.dsSelectWindow = new DistributionSetSelectWindow(i18n, eventBus, notification, targetManagement, - targetFilterQueryManagement); + targetFilterQueryManagement, entityFactory); setStyleName("sp-table"); setSizeFull(); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/TargetAssignmentOperations.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/TargetAssignmentOperations.java index 59f101f11..240cf9126 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/TargetAssignmentOperations.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/TargetAssignmentOperations.java @@ -105,7 +105,7 @@ public final class TargetAssignmentOperations { final String maintenanceTimeZone = maintenanceWindowLayout.getMaintenanceTimeZone(); final Set dsIds = distributionSets.stream().map(DistributionSet::getId).collect(Collectors.toSet()); - final List deploymentRequests = new ArrayList<>(); + final List deploymentRequests = new ArrayList<>(); dsIds.forEach(dsId -> targets.forEach(t -> { final DeploymentRequestBuilder request = DeploymentManagement.deploymentRequest(t.getControllerId(), dsId) .setActionType(actionType).setForceTime(forcedTimeStamp);