From 868751013148f287bcb55ed4ba94f53fbdf7eec5 Mon Sep 17 00:00:00 2001 From: Stefan Klotz <35995139+StefanKlt@users.noreply.github.com> Date: Tue, 17 Sep 2019 14:20:26 +0200 Subject: [PATCH] Assign multiple distribution sets to a target via mgmt api (#886) * Add multiassignment to mgmt api target endpoint * Remove single assignment ds to targets offline * Fix tests * Add quota for maxResultingActionsPerManualAssignment * Fix assignment with same target or distribution set multiple times in one request * Log UI error * Add tests * Enable single assignment requests with multiple DSs and types * Remove redundant target to DS assignment methods * Add tests, fix assignment * Fix possible nullpointer during target assignment request * Update api docu * Clean up deployment management code * Enforce MaxActions quota for offline assignment * Fix review findings * Rename property, add migration into * Add builder for DeploymentRequest * Change offline assignment method to accept an assignment list, like online assignment * Fix PR findings Signed-off-by: Stefan Klotz --- MIGRATION.md | 10 +- .../hawkbit/exception/SpServerError.java | 8 +- ...ticationMessageHandlerIntegrationTest.java | 17 +- ...ssageDispatcherServiceIntegrationTest.java | 5 - .../repository/DeploymentManagement.java | 137 +++++------ .../hawkbit/repository/QuotaManagement.java | 6 +- .../MultiAssignmentIsNotEnabledException.java | 62 +++++ .../exception/QuotaExceededException.java | 3 +- .../repository/model/DeploymentRequest.java | 105 +++++++++ .../model/DeploymentRequestBuilder.java | 101 +++++++++ .../model/TargetWithActionType.java | 49 +++- .../repository/PropertiesQuotaManagement.java | 4 +- .../jpa/AbstractDsAssignmentStrategy.java | 29 +-- .../jpa/JpaDeploymentManagement.java | 208 +++++++++-------- .../jpa/OfflineDsAssignmentStrategy.java | 26 ++- .../jpa/OnlineDsAssignmentStrategy.java | 14 +- .../jpa/autoassign/AutoAssignChecker.java | 19 +- .../repository/jpa/utils/QuotaHelper.java | 30 ++- .../jpa/ControllerManagementTest.java | 5 +- .../jpa/DeploymentManagementTest.java | 212 +++++++++++++----- .../repository/jpa/RolloutManagementTest.java | 9 +- .../jpa/autoassign/AutoAssignCheckerTest.java | 11 +- .../autocleanup/AutoActionCleanupTest.java | 41 ++-- .../test/util/AbstractIntegrationTest.java | 81 +++++-- .../rest/resource/DdiDeploymentBaseTest.java | 24 +- .../hawkbit/mgmt/json/model/MgmtId.java | 26 ++- .../MgmtTargetAssignmentRequestBody.java | 16 +- .../MgmtSoftwareModuleTypeAssigment.java | 1 - .../target/MgmtDistributionSetAssignment.java | 16 +- .../MgmtDistributionSetAssignments.java | 56 +++++ ...istributionSetAssignmentsDeserializer.java | 51 +++++ .../mgmt/rest/api/MgmtTargetRestApi.java | 8 +- .../resource/MgmtDeploymentRequestMapper.java | 74 ++++++ .../resource/MgmtDistributionSetMapper.java | 14 ++ .../resource/MgmtDistributionSetResource.java | 43 ++-- .../rest/resource/MgmtTargetResource.java | 47 ++-- .../MgmtDistributionSetResourceTest.java | 72 +++++- .../rest/resource/MgmtTargetResourceTest.java | 75 ++++++- .../exception/ResponseExceptionHandler.java | 1 + .../src/main/asciidoc/targets-api-guide.adoc | 58 ++++- .../src/main/errors/400_multiassignment.adoc | 3 + .../RootControllerDocumentationTest.java | 18 +- .../AbstractApiRestDocumentation.java | 8 +- .../documentation/MgmtApiModelProperties.java | 2 +- .../DistributionSetsDocumentationTest.java | 25 ++- .../TargetResourceDocumentationTest.java | 101 ++++++--- .../security/HawkbitSecurityProperties.java | 21 +- .../TargetAssignmentOperations.java | 60 +++-- .../targettable/BulkUploadHandler.java | 12 +- .../src/main/resources/messages.properties | 1 + 50 files changed, 1466 insertions(+), 559 deletions(-) create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/MultiAssignmentIsNotEnabledException.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DeploymentRequest.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DeploymentRequestBuilder.java create mode 100644 hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignments.java create mode 100644 hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignmentsDeserializer.java create mode 100644 hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDeploymentRequestMapper.java create mode 100644 hawkbit-rest/hawkbit-rest-docs/src/main/errors/400_multiassignment.adoc diff --git a/MIGRATION.md b/MIGRATION.md index f9d9fff51..26ce2f028 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -13,8 +13,12 @@ - ENTITYsrest classes have been removed; List used instead (e.g. List instead of TargetsRest) ### Renamed api annotations -- Annotation `org.eclipse.hawkbit.rest.resource.EnableRestResources` have changed to `org.eclipse.hawkbit.mgmt.annotation.EnableMgmtApi` -- Annotation `org.eclipse.hawkbit.ddi.resource.EnableDirectDeviceApi` have changed to `org.eclipse.hawkbit.ddi.annotation.EnableDdiApi` +- Annotation `org.eclipse.hawkbit.rest.resource.EnableRestResources` has changed to `org.eclipse.hawkbit.mgmt.annotation.EnableMgmtApi` +- Annotation `org.eclipse.hawkbit.ddi.resource.EnableDirectDeviceApi` has changed to `org.eclipse.hawkbit.ddi.annotation.EnableDdiApi` ### Renamed maven modules -- Module hawkbit-mgmt-api-client have changed to hawkbit-example-mgmt-simulator \ No newline at end of file +- Module hawkbit-mgmt-api-client has changed to hawkbit-example-mgmt-simulator + +## Milestone 0.3.0M6 +### Configuration Property changes +- hawkbit.server.security.dos.maxTargetsPerManualAssignment has changed to hawkbit.server.security.dos.maxTargetDistributionSetAssignmentsPerManualAssignment 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 21182cd26..389f8edb9 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 @@ -240,7 +240,13 @@ public enum SpServerError { * change is not allowed. */ SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED("hawkbit.server.error.repo.tenantConfigurationValueChangeNotAllowed", - "The requested tenant configuration value modification is not allowed."); + "The requested tenant configuration value modification is not allowed."), + + /** + * + */ + SP_MULTIASSIGNMENT_NOT_ENABLED("hawkbit.server.error.multiassignmentNotEnabled", + "The requested operation requires Multiassignments to be enabled."); private final String key; private final String message; diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpAuthenticationMessageHandlerIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpAuthenticationMessageHandlerIntegrationTest.java index 4a868ceb4..38b2d3176 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpAuthenticationMessageHandlerIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpAuthenticationMessageHandlerIntegrationTest.java @@ -11,7 +11,6 @@ package org.eclipse.hawkbit.integration; import static org.assertj.core.api.Assertions.assertThat; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import org.eclipse.hawkbit.amqp.AmqpProperties; @@ -23,7 +22,6 @@ import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; -import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.security.DmfTenantSecurityToken; import org.eclipse.hawkbit.security.DmfTenantSecurityToken.FileResource; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; @@ -193,8 +191,7 @@ public class AmqpAuthenticationMessageHandlerIntegrationTest extends AbstractAmq final DmfTenantSecurityToken securityToken = createTenantSecurityToken(TENANT_EXIST, TARGET, FileResource.createFileResourceBySha1(artifact.getSha1Hash())); - deploymentManagement.assignDistributionSet(distributionSet.getId(), - Arrays.asList(new TargetWithActionType(TARGET))); + assignDistributionSet(distributionSet.getId(), TARGET); final Message returnMessage = sendAndReceiveAuthenticationMessage(securityToken); verifyOkResult(returnMessage, artifact); @@ -212,8 +209,7 @@ public class AmqpAuthenticationMessageHandlerIntegrationTest extends AbstractAmq final DmfTenantSecurityToken securityToken = createTenantSecurityToken(TENANT_EXIST, target.getId(), null, FileResource.createFileResourceBySha1(artifact.getSha1Hash())); - deploymentManagement.assignDistributionSet(distributionSet.getId(), - Arrays.asList(new TargetWithActionType(TARGET))); + assignDistributionSet(distributionSet.getId(), TARGET); final Message returnMessage = sendAndReceiveAuthenticationMessage(securityToken); verifyOkResult(returnMessage, artifact); @@ -246,8 +242,7 @@ public class AmqpAuthenticationMessageHandlerIntegrationTest extends AbstractAmq final DmfTenantSecurityToken securityToken = createTenantSecurityToken(TENANT_EXIST, TARGET, FileResource.createFileResourceBySha1(artifact.getSha1Hash())); - deploymentManagement.assignDistributionSet(distributionSet.getId(), - Arrays.asList(new TargetWithActionType(TARGET))); + assignDistributionSet(distributionSet.getId(), TARGET); final Message returnMessage = sendAndReceiveAuthenticationMessage(securityToken); verifyOkResult(returnMessage, artifact); @@ -309,8 +304,7 @@ public class AmqpAuthenticationMessageHandlerIntegrationTest extends AbstractAmq final FileResource fileResource = FileResource.createFileResourceByArtifactId(artifact.getId()); final DmfTenantSecurityToken securityToken = createTenantSecurityToken(TENANT_EXIST, TARGET, fileResource); - deploymentManagement.assignDistributionSet(distributionSet.getId(), - Arrays.asList(new TargetWithActionType(TARGET))); + assignDistributionSet(distributionSet.getId(), TARGET); final Message returnMessage = sendAndReceiveAuthenticationMessage(securityToken); verifyOkResult(returnMessage, artifact); @@ -365,8 +359,7 @@ public class AmqpAuthenticationMessageHandlerIntegrationTest extends AbstractAmq softwareModule.getArtifact(artifact.getId()).get().getFilename()); final DmfTenantSecurityToken securityToken = createTenantSecurityToken(TENANT_EXIST, TARGET, fileResource); - deploymentManagement.assignDistributionSet(distributionSet.getId(), - Arrays.asList(new TargetWithActionType(TARGET))); + assignDistributionSet(distributionSet.getId(), TARGET); final Message returnMessage = sendAndReceiveAuthenticationMessage(securityToken); verifyOkResult(returnMessage, artifact); 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 dc9544ec3..50ea5a8d4 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 @@ -57,7 +57,6 @@ import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; -import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.junit.Test; import org.mockito.Mockito; import org.springframework.amqp.core.Message; @@ -464,10 +463,6 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer createConditionFactory().until(() -> securityRule.runAsPrivileged(callable)); } - private void enableMultiAssignments() { - tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, true); - } - private void assertLatestMultiActionMessageContainsInstallMessages(final String controllerId, final List> smIdsOfActionsExpected) { final Message multiactionMessage = replyToListener.getLatestEventMessage(EventTopic.MULTI_ACTION); 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 4e76e1b15..aec6bc648 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 @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.repository; import java.util.Collection; import java.util.List; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; @@ -21,20 +22,21 @@ import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEv import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.model.Action; -import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.DeploymentRequest; +import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; -import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -48,71 +50,12 @@ import org.springframework.security.access.prepost.PreAuthorize; public interface DeploymentManagement { /** - * Assigns the addressed {@link DistributionSet} to all {@link Target}s by - * their IDs with a specific {@link ActionType} and {@code forcetime}. + * Assigns {@link DistributionSet}s to {@link Target}s according to the + * {@link DeploymentRequest}. * - * @param dsID - * the ID of the distribution set to assign - * @param actionType - * the type of the action to apply on the assignment - * @param forcedTimestamp - * the time when the action should be forced, only necessary for - * {@link ActionType#TIMEFORCED} - * @param controllerIDs - * the IDs of the target to assign the distribution set - * @return the assignment result - * - * @throws IncompleteDistributionSetException - * if mandatory {@link SoftwareModuleType} are not assigned as - * define by the {@link DistributionSetType}. - * - * @throws EntityNotFoundException - * if either provided {@link DistributionSet} or {@link Target}s - * do not exist + * @param deploymentRequests + * information about all target-ds-assignments that shall be made * - * @throws QuotaExceededException - * if the maximum number of targets the distribution set can be - * assigned to at once is exceeded - */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) - DistributionSetAssignmentResult assignDistributionSet(long dsID, @NotNull ActionType actionType, - long forcedTimestamp, @NotEmpty Collection controllerIDs); - - /** - * Assigns the addressed {@link DistributionSet} to all {@link Target}s by - * their IDs with a specific {@link ActionType} and {@code forcetime}. - * - * @param dsID - * the ID of the distribution set to assign - * @param targets - * a list of all targets and their action type - * @return the assignment result - * - * @throws IncompleteDistributionSetException - * if mandatory {@link SoftwareModuleType} are not assigned as - * define by the {@link DistributionSetType}. - * - * @throws EntityNotFoundException - * if either provided {@link DistributionSet} or {@link Target}s - * do not exist - * - * @throws QuotaExceededException - * if the maximum number of targets the distribution set can be - * assigned to at once is exceeded - */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) - DistributionSetAssignmentResult assignDistributionSet(long dsID, - @NotEmpty Collection targets); - - /** - * Assigns the given set of {@link DistributionSet} entities to the given - * {@link Target}s using the specified {@link ActionType} and - * {@code forcetime}. - * - * @param dsIDs - * the set of IDs of the distribution sets to assign - * @param targets - * a list of all targets and their action type * @return the list of assignment results * * @throws IncompleteDistributionSetException @@ -126,26 +69,29 @@ public interface DeploymentManagement { * @throws QuotaExceededException * if the maximum number of targets the distribution set can be * assigned to at once is exceeded + * @throws MultiAssignmentIsNotEnabledException + * if the request results in multiple assignments to the same + * target and multiassignment is disabled + * */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) - List assignDistributionSets(@NotEmpty Set dsIDs, - @NotEmpty Collection targets); + List assignDistributionSets( + @NotEmpty List deploymentRequests); /** - * Assigns the addressed {@link DistributionSet} to all {@link Target}s by - * their IDs with a specific {@link ActionType} and an action message. + * Assigns {@link DistributionSet}s to {@link Target}s according to the + * {@link DeploymentRequest}. * - * @param dsID - * the ID of the distribution set to assign - * @param targets - * a list of all targets and their action type + * @param deploymentRequests + * information about all target-ds-assignments that shall be made * @param actionMessage * an optional message for the action status - * @return the assignment result + * + * @return the list of assignment results * * @throws IncompleteDistributionSetException * if mandatory {@link SoftwareModuleType} are not assigned as - * define by the {@link DistributionSetType}. + * defined by the {@link DistributionSetType}. * * @throws EntityNotFoundException * if either provided {@link DistributionSet} or {@link Target}s @@ -154,14 +100,32 @@ public interface DeploymentManagement { * @throws QuotaExceededException * if the maximum number of targets the distribution set can be * assigned to at once is exceeded + * @throws MultiAssignmentIsNotEnabledException + * if the request results in multiple assignments to the same + * target and multiassignment is disabled + * */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) - DistributionSetAssignmentResult assignDistributionSet(long dsID, @NotEmpty Collection targets, - String actionMessage); + List assignDistributionSets( + @NotEmpty List deploymentRequests, String actionMessage); /** - * Registers an "offline" assignment, i.e. adds a completed action for the - * given {@link DistributionSet} to the given {@link Target}s. + * build a {@link DeploymentRequest} for a target distribution set + * assignment + * + * @param controllerId + * ID of target + * @param distributionSetId + * ID of distribution set + * @return the builder + */ + static DeploymentRequestBuilder deploymentRequest(final String controllerId, final long distributionSetId) { + return new DeploymentRequestBuilder(controllerId, distributionSetId); + } + + /** + * Registers "offline" assignments. "offline" assignment means adding a + * completed action for a {@link DistributionSet} to a {@link Target}. * * The handling differs to hawkBit-managed updates by means that:
* @@ -174,11 +138,10 @@ public interface DeploymentManagement { *
  • does not send a {@link TargetAssignDistributionSetEvent}.
  • * * - * @param dsID - * the ID of the distribution set that was assigned - * @param controllerIDs - * a list of IDs of the targets that where assigned - * @return the assignment result + * @param assignments + * target IDs with the respective distribution set ID which they + * are supposed to be assigned to + * @return the assignment results * * @throws IncompleteDistributionSetException * if mandatory {@link SoftwareModuleType} are not assigned as @@ -191,9 +154,13 @@ public interface DeploymentManagement { * @throws QuotaExceededException * if the maximum number of targets the distribution set can be * assigned to at once is exceeded + * + * @throws MultiAssignmentIsNotEnabledException + * if the request results in multiple assignments to the same + * target and multiassignment is disabled */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) - DistributionSetAssignmentResult offlineAssignedDistributionSet(Long dsID, Collection controllerIDs); + List offlineAssignedDistributionSets(Collection> assignments); /** * Cancels the {@link Action} with the given ID. The method will immediately diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java index c769f4d73..378801d02 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java @@ -80,10 +80,10 @@ public interface QuotaManagement { int getMaxTargetsPerRolloutGroup(); /** - * @return the maximum number of targets which for a manual distribution set - * assignment + * @return the maximum number of target distribution set assignments + * resulting from a manual assignment */ - int getMaxTargetsPerManualAssignment(); + int getMaxTargetDistributionSetAssignmentsPerManualAssignment(); /** * @return the maximum number of targets for an automatic distribution set diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/MultiAssignmentIsNotEnabledException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/MultiAssignmentIsNotEnabledException.java new file mode 100644 index 000000000..49e641db9 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/MultiAssignmentIsNotEnabledException.java @@ -0,0 +1,62 @@ +/** + * 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 an operation requires multiassignments, but the + * feature is not enabled. + * + */ +public class MultiAssignmentIsNotEnabledException extends AbstractServerRtException { + + private static final long serialVersionUID = 1L; + private static final SpServerError THIS_ERROR = SpServerError.SP_MULTIASSIGNMENT_NOT_ENABLED; + + /** + * Default constructor. + */ + public MultiAssignmentIsNotEnabledException() { + super(THIS_ERROR); + } + + /** + * Parameterized constructor. + * + * @param cause + * of the exception + */ + public MultiAssignmentIsNotEnabledException(final Throwable cause) { + super(THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + * @param cause + * of the exception + */ + public MultiAssignmentIsNotEnabledException(final String message, final Throwable cause) { + super(message, THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + */ + public MultiAssignmentIsNotEnabledException(final String message) { + super(message, THIS_ERROR); + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/QuotaExceededException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/QuotaExceededException.java index 6a013836f..365fbc416 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/QuotaExceededException.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/QuotaExceededException.java @@ -120,7 +120,8 @@ public final class QuotaExceededException extends AbstractServerRtException { * The maximum number of entities that can be assigned to the * parent entity. */ - public QuotaExceededException(final String type, final String parentType, final Long parentId, final long requested, + public QuotaExceededException(final String type, final String parentType, final Object parentId, + final long requested, final long quota) { super(String.format(ASSIGNMENT_QUOTA_EXCEEDED_MESSAGE, requested, type, parentType, parentId != null ? String.valueOf(parentId) : "", quota), SpServerError.SP_QUOTA_EXCEEDED); 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 new file mode 100644 index 000000000..b1c00aa24 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DeploymentRequest.java @@ -0,0 +1,105 @@ +/** + * 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.model; + +import java.util.Objects; + +import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException; +import org.eclipse.hawkbit.repository.model.Action.ActionType; + +/** + * A custom view on assigning a {@link DistributionSet} to a {@link Target}. + * + */ +public class DeploymentRequest { + private final Long distributionSetId; + private final TargetWithActionType targetWithActionType; + + /** + * Constructor that also accepts maintenance schedule parameters and checks + * for validity of the specified maintenance schedule. + * + * @param controllerId + * for which the action is created. + * @param distributionSetId + * of the distribution set that that should be assigned to the + * controller. + * @param actionType + * specified for the action. + * @param forceTime + * at what time the type soft turns into forced. + * @param maintenanceSchedule + * is the cron expression to be used for scheduling maintenance + * windows. Expression has 6 mandatory fields and 1 last optional + * field: "second minute hour dayofmonth month weekday year" + * @param maintenanceWindowDuration + * in HH:mm:ss format specifying the duration of a maintenance + * window, for example 00:30:00 for 30 minutes + * @param maintenanceWindowTimeZone + * is the time zone specified as +/-hh:mm offset from UTC, for + * example +02:00 for CET summer time and +00:00 for UTC. The + * start time of a maintenance window calculated based on the + * cron expression is relative to this time zone. + * + * @throws InvalidMaintenanceScheduleException + * 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); + this.distributionSetId = distributionSetId; + } + + public Long getDistributionSetId() { + return distributionSetId; + } + + public String getControllerId() { + return targetWithActionType.getControllerId(); + } + + public TargetWithActionType getTargetWithActionType() { + return targetWithActionType; + } + + + @Override + public String toString() { + return String.format( + "DeploymentRequest [controllerId=%s, distributionSetId=%d, actionType=%s, forceTime=%d, maintenanceSchedule=%s, maintenanceWindowDuration=%s, maintenanceWindowTimeZone=%s]", + targetWithActionType.getControllerId(), getDistributionSetId(), targetWithActionType.getActionType(), + targetWithActionType.getForceTime(), targetWithActionType.getMaintenanceSchedule(), + targetWithActionType.getMaintenanceWindowDuration(), + targetWithActionType.getMaintenanceWindowTimeZone()); + } + + @Override + public int hashCode() { + return Objects.hash(distributionSetId, targetWithActionType); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DeploymentRequest other = (DeploymentRequest) obj; + return Objects.equals(distributionSetId, other.distributionSetId) + && Objects.equals(targetWithActionType, other.targetWithActionType); + } +} 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 new file mode 100644 index 000000000..8386ef730 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DeploymentRequestBuilder.java @@ -0,0 +1,101 @@ +/** + * 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.model; + +import org.eclipse.hawkbit.repository.model.Action.ActionType; + +/** + * Builder for {@link DeploymentRequest} + * + */ +public class DeploymentRequestBuilder { + + private final String controllerId; + private final Long distributionSetId; + + private long forceTime = RepositoryModelConstants.NO_FORCE_TIME; + private ActionType actionType = ActionType.FORCED; + private String maintenanceSchedule; + private String maintenanceWindowDuration; + private String maintenanceWindowTimeZone; + + /** + * Create a builder for a target distribution set assignment with the + * mandatory fields + * + * @param controllerId + * ID of the target + * @param distributionSetId + * ID of the distribution set + */ + public DeploymentRequestBuilder(final String controllerId, final Long distributionSetId) { + this.controllerId = controllerId; + this.distributionSetId = distributionSetId; + } + + /** + * Set an other {@link ActionType} than {@link ActionType#FORCED} + * + * @param actionType + * type to used + * @return builder + */ + public DeploymentRequestBuilder setActionType(final ActionType actionType) { + this.actionType = actionType; + return this; + } + + /** + * Set a forceTime other than the default one. + * + * @param forceTime + * at what time the type soft turns into forced. + * @return builder + */ + public DeploymentRequestBuilder setForceTime(final long forceTime) { + this.forceTime = forceTime; + return this; + } + + /** + * Set a maintenanceWindow + * + * @param maintenanceSchedule + * is the cron expression to be used for scheduling maintenance + * windows. Expression has 6 mandatory fields and 1 last optional + * field: "second minute hour dayofmonth month weekday year" + * @param maintenanceWindowDuration + * in HH:mm:ss format specifying the duration of a maintenance + * window, for example 00:30:00 for 30 minutes + * @param maintenanceWindowTimeZone + * is the time zone specified as +/-hh:mm offset from UTC, for + * example +02:00 for CET summer time and +00:00 for UTC. The + * start time of a maintenance window calculated based on the + * cron expression is relative to this time zone. + * @return builder + */ + public DeploymentRequestBuilder setMaintenance(final String maintenanceSchedule, + final String maintenanceWindowDuration, final String maintenanceWindowTimeZone) { + this.maintenanceSchedule = maintenanceSchedule; + this.maintenanceWindowDuration = maintenanceWindowDuration; + this.maintenanceWindowTimeZone = maintenanceWindowTimeZone; + return this; + } + + /** + * build the request + * + * @return the request object + */ + public DeploymentRequest build() { + return new DeploymentRequest(controllerId, distributionSetId, actionType, forceTime, maintenanceSchedule, + maintenanceWindowDuration, maintenanceWindowTimeZone); + } + +} 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 2391bbea5..ac48f3849 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 @@ -8,6 +8,8 @@ */ package org.eclipse.hawkbit.repository.model; +import java.util.Objects; + import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException; import org.eclipse.hawkbit.repository.model.Action.ActionType; @@ -16,7 +18,6 @@ import org.eclipse.hawkbit.repository.model.Action.ActionType; * */ public class TargetWithActionType { - private final String controllerId; private final ActionType actionType; private final long forceTime; @@ -24,10 +25,27 @@ public class TargetWithActionType { private String maintenanceWindowDuration; private String maintenanceWindowTimeZone; + /** + * Constructor that uses {@link ActionType#FORCED} + * + * @param controllerId + * ID if the controller + */ public TargetWithActionType(final String controllerId) { this(controllerId, ActionType.FORCED, 0); } + /** + * Constructor that leaves the maintenance info empty + * + * @param controllerId + * for which the action is created. + * @param actionType + * specified for the action. + * @param forceTime + * after that point in time the action is exposed as forcen in + * case the type is {@link ActionType#TIMEFORCED} + */ public TargetWithActionType(final String controllerId, final ActionType actionType, final long forceTime) { this.controllerId = controllerId; this.actionType = actionType != null ? actionType : ActionType.FORCED; @@ -42,6 +60,9 @@ public class TargetWithActionType { * for which the action is created. * @param actionType * specified for the action. + * @param forceTime + * after that point in time the action is exposed as forcen in + * case the type is {@link ActionType#TIMEFORCED} * @param maintenanceSchedule * is the cron expression to be used for scheduling maintenance * windows. Expression has 6 mandatory fields and 1 last optional @@ -122,4 +143,30 @@ public class TargetWithActionType { + "]"; } + @Override + public int hashCode() { + return Objects.hash(actionType, controllerId, forceTime, maintenanceSchedule, maintenanceWindowDuration, + maintenanceWindowTimeZone); + } + + @SuppressWarnings("squid:S1067") + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TargetWithActionType other = (TargetWithActionType) obj; + return Objects.equals(actionType, other.actionType) && Objects.equals(controllerId, other.controllerId) + && Objects.equals(forceTime, other.forceTime) + && 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/PropertiesQuotaManagement.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java index 001f5f9dc..ff05dac18 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java @@ -89,8 +89,8 @@ public class PropertiesQuotaManagement implements QuotaManagement { } @Override - public int getMaxTargetsPerManualAssignment() { - return securityProperties.getDos().getMaxTargetsPerManualAssignment(); + public int getMaxTargetDistributionSetAssignmentsPerManualAssignment() { + return securityProperties.getDos().getMaxTargetDistributionSetAssignmentsPerManualAssignment(); } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java index d52396243..09a203edc 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java @@ -10,9 +10,9 @@ package org.eclipse.hawkbit.repository.jpa; import java.util.Collection; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.BooleanSupplier; import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.QuotaManagement; @@ -51,17 +51,19 @@ public abstract class AbstractDsAssignmentStrategy { protected final ActionRepository actionRepository; private final ActionStatusRepository actionStatusRepository; private final QuotaManagement quotaManagement; + private final BooleanSupplier multiAssignmentsConfig; AbstractDsAssignmentStrategy(final TargetRepository targetRepository, final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder, final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository, - final QuotaManagement quotaManagement) { + final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig) { this.targetRepository = targetRepository; this.afterCommit = afterCommit; this.eventPublisherHolder = eventPublisherHolder; this.actionRepository = actionRepository; this.actionStatusRepository = actionStatusRepository; this.quotaManagement = quotaManagement; + this.multiAssignmentsConfig = multiAssignmentsConfig; } /** @@ -199,14 +201,14 @@ public abstract class AbstractDsAssignmentStrategy { new CancelTargetAssignmentEvent(target, actionId, eventPublisherHolder.getApplicationId()))); } - JpaAction createTargetAction(final Map targetsWithActionMap, final JpaTarget target, + JpaAction createTargetAction(final TargetWithActionType targetWithActionType, final List targets, final JpaDistributionSet set) { - - // enforce the 'max actions per target' quota - assertActionsPerTargetQuota(target, 1); + final Optional optTarget = targets.stream() + .filter(t -> t.getControllerId().equals(targetWithActionType.getControllerId())).findFirst(); // create the action - return getTargetWithActionType(targetsWithActionMap, target.getControllerId()).map(targetWithActionType -> { + return optTarget.map(target -> { + assertActionsPerTargetQuota(target, 1); final JpaAction actionForTarget = new JpaAction(); actionForTarget.setActionType(targetWithActionType.getActionType()); actionForTarget.setForcedTime(targetWithActionType.getForceTime()); @@ -218,7 +220,7 @@ public abstract class AbstractDsAssignmentStrategy { actionForTarget.setMaintenanceWindowTimeZone(targetWithActionType.getMaintenanceWindowTimeZone()); return actionForTarget; }).orElseGet(() -> { - LOG.warn("Cannot find targetWithActionType for target '{}'.", target.getControllerId()); + LOG.warn("Cannot find target for targetWithActionType '{}'.", targetWithActionType.getControllerId()); return null; }); } @@ -241,14 +243,7 @@ public abstract class AbstractDsAssignmentStrategy { actionRepository::countByTargetId); } - private static Optional getTargetWithActionType( - final Map targetsWithActionMap, final String controllerId) { - if (targetsWithActionMap.containsKey(controllerId)) { - return Optional.of(targetsWithActionMap.get(controllerId)); - } else { - return targetsWithActionMap.values().stream() - .filter(t -> controllerId.equalsIgnoreCase(t.getControllerId())).findFirst(); - } + protected boolean isMultiAssignmentsEnabled() { + return multiAssignmentsConfig.getAsBoolean(); } - } 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 bf9e61839..107921e57 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 @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -43,6 +44,7 @@ import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.ForceQuitActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; @@ -59,6 +61,7 @@ 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.ActionStatus; +import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.DistributionSetType; @@ -81,8 +84,12 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.domain.Specification; import org.springframework.orm.jpa.vendor.Database; +import org.springframework.retry.RetryCallback; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; +import org.springframework.retry.backoff.FixedBackOffPolicy; +import org.springframework.retry.policy.SimpleRetryPolicy; +import org.springframework.retry.support.RetryTemplate; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; @@ -131,6 +138,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { private final SystemSecurityContext systemSecurityContext; private final TenantAware tenantAware; private final Database database; + private final RetryTemplate retryTemplate; protected JpaDeploymentManagement(final EntityManager entityManager, final ActionRepository actionRepository, final DistributionSetRepository distributionSetRepository, final TargetRepository targetRepository, @@ -150,80 +158,88 @@ public class JpaDeploymentManagement implements DeploymentManagement { onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository, quotaManagement, this::isMultiAssignmentsEnabled); offlineDsAssignmentStrategy = new OfflineDsAssignmentStrategy(targetRepository, afterCommit, - eventPublisherHolder, actionRepository, actionStatusRepository, quotaManagement); + eventPublisherHolder, actionRepository, actionStatusRepository, quotaManagement, + this::isMultiAssignmentsEnabled); this.tenantConfigurationManagement = tenantConfigurationManagement; this.quotaManagement = quotaManagement; this.systemSecurityContext = systemSecurityContext; this.tenantAware = tenantAware; this.database = database; + retryTemplate = createRetryTemplate(); } @Override @Transactional(isolation = Isolation.READ_COMMITTED) - @Retryable(include = { - ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) - public DistributionSetAssignmentResult offlineAssignedDistributionSet(final Long dsID, - final Collection controllerIDs) { - final DistributionSetAssignmentResult result = assignDistributionSetToTargets(dsID, - controllerIDs.stream() - .map(controllerId -> new TargetWithActionType(controllerId, ActionType.FORCED, -1)) - .collect(Collectors.toList()), - null, offlineDsAssignmentStrategy); - offlineDsAssignmentStrategy.sendDeploymentEvents(result); - return result; - } - - @Override - @Transactional(isolation = Isolation.READ_COMMITTED) - @Retryable(include = { - ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) - public DistributionSetAssignmentResult assignDistributionSet(final long dsID, final ActionType actionType, - final long forcedTimestamp, final Collection controllerIDs) { - - final DistributionSetAssignmentResult result = assignDistributionSetToTargets(dsID, - controllerIDs.stream() - .map(controllerId -> new TargetWithActionType(controllerId, actionType, forcedTimestamp)) - .collect(Collectors.toList()), - null, onlineDsAssignmentStrategy); - onlineDsAssignmentStrategy.sendDeploymentEvents(result); - return result; - } - - @Override - @Transactional(isolation = Isolation.READ_COMMITTED) - @Retryable(include = { - ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) - public DistributionSetAssignmentResult assignDistributionSet(final long dsID, - final Collection targets) { - - final DistributionSetAssignmentResult result = assignDistributionSetToTargets(dsID, targets, null, - onlineDsAssignmentStrategy); - onlineDsAssignmentStrategy.sendDeploymentEvents(result); - return result; - } - - @Override - public List assignDistributionSets(final Set dsIDs, - final Collection targets) { - - final List results = dsIDs.stream() - .map(dsID -> assignDistributionSetToTargets(dsID, targets, null, onlineDsAssignmentStrategy)) + public List offlineAssignedDistributionSets( + final Collection> assignments) { + final Collection> distinctAssignments = assignments.stream().distinct() .collect(Collectors.toList()); - onlineDsAssignmentStrategy.sendDeploymentEvents(results); + + enforceMaxAssignmentsPerRequest(distinctAssignments.size()); + final List deploymentRequests = distinctAssignments.stream() + .map(entry -> DeploymentManagement.deploymentRequest(entry.getKey(), entry.getValue()).build()) + .collect(Collectors.toList()); + + return assignDistributionSets(deploymentRequests, null, offlineDsAssignmentStrategy); + } + + @Override + @Transactional(isolation = Isolation.READ_COMMITTED) + public List assignDistributionSets( + final List deploymentRequests) { + return assignDistributionSets(deploymentRequests, null); + } + + @Override + @Transactional(isolation = Isolation.READ_COMMITTED) + public List assignDistributionSets( + final List deploymentRequests, final String actionMessage) { + return assignDistributionSets(deploymentRequests, actionMessage, onlineDsAssignmentStrategy); + } + + private List assignDistributionSets( + final List deploymentRequests, final String actionMessage, + final AbstractDsAssignmentStrategy strategy) { + final List validatedRequests = validateRequestForAssignments(deploymentRequests); + final Map> assignmentsByDsIds = convertRequest(validatedRequests); + + final List results = assignmentsByDsIds.entrySet().stream() + .map(entry -> assignDistributionSetToTargetsWithRetry(entry.getKey(), entry.getValue(), actionMessage, + strategy)) + .collect(Collectors.toList()); + strategy.sendDeploymentEvents(results); return results; } - @Override - @Transactional(isolation = Isolation.READ_COMMITTED) - @Retryable(include = { - ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) - public DistributionSetAssignmentResult assignDistributionSet(final long dsID, - final Collection targets, final String actionMessage) { + private List validateRequestForAssignments(List deploymentRequests) { + if (!isMultiAssignmentsEnabled()) { + deploymentRequests = deploymentRequests.stream().distinct().collect(Collectors.toList()); + checkIfRequiresMultiAssignment(deploymentRequests); + } + checkQuotaForAssignment(deploymentRequests); + return deploymentRequests; + } - final DistributionSetAssignmentResult result = assignDistributionSetToTargets(dsID, targets, actionMessage, - onlineDsAssignmentStrategy); - onlineDsAssignmentStrategy.sendDeploymentEvents(result); - return result; + private static Map> convertRequest( + final Collection deploymentRequests) { + return deploymentRequests.stream().collect(Collectors.groupingBy(DeploymentRequest::getDistributionSetId, + Collectors.mapping(DeploymentRequest::getTargetWithActionType, Collectors.toList()))); + } + + private static void checkIfRequiresMultiAssignment(final Collection deploymentRequests) { + final long distinctTargetsInRequest = deploymentRequests.stream() + .map(request -> request.getTargetWithActionType().getControllerId()).distinct().count(); + if (distinctTargetsInRequest < deploymentRequests.size()) { + throw new MultiAssignmentIsNotEnabledException(); + } + } + + private DistributionSetAssignmentResult assignDistributionSetToTargetsWithRetry(final Long dsID, + final Collection targetsWithActionType, final String actionMessage, + final AbstractDsAssignmentStrategy assignmentStrategy) { + final RetryCallback retryCallback = retryContext -> assignDistributionSetToTargets( + dsID, targetsWithActionType, actionMessage, assignmentStrategy); + return retryTemplate.execute(retryCallback); } /** @@ -259,10 +275,10 @@ public class JpaDeploymentManagement implements DeploymentManagement { final AbstractDsAssignmentStrategy assignmentStrategy) { final JpaDistributionSet distributionSetEntity = getAndValidateDsById(dsID); - checkQuotaForAssignment(targetsWithActionType, distributionSetEntity); + final List targetIds = targetsWithActionType.stream().map(TargetWithActionType::getControllerId).distinct() + .collect(Collectors.toList()); - final List targetEntities = assignmentStrategy.findTargetsForAssignment( - targetsWithActionType.stream().map(TargetWithActionType::getControllerId).collect(Collectors.toList()), + final List targetEntities = assignmentStrategy.findTargetsForAssignment(targetIds, distributionSetEntity.getId()); if (targetEntities.isEmpty()) { @@ -288,7 +304,10 @@ public class JpaDeploymentManagement implements DeploymentManagement { final AbstractDsAssignmentStrategy assignmentStrategy, final JpaDistributionSet distributionSetEntity, final List targetEntities) { final List> targetEntitiesIdsChunks = getTargetEntitiesAsChunks(targetEntities); - closeOrCancelActiveActions(assignmentStrategy, targetEntitiesIdsChunks); + + if (!isMultiAssignmentsEnabled()) { + closeOrCancelActiveActions(assignmentStrategy, targetEntitiesIdsChunks); + } // cancel all scheduled actions which are in-active, these actions were // not active before and the manual assignment which has been done // cancels them @@ -320,7 +339,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { private static DistributionSetAssignmentResult buildAssignmentResult(final JpaDistributionSet distributionSet, final List assignedActions, final int totalTargetsForAssignment) { - int alreadyAssignedTargetsCount = totalTargetsForAssignment - assignedActions.size(); + final int alreadyAssignedTargetsCount = totalTargetsForAssignment - assignedActions.size(); return new DistributionSetAssignmentResult(distributionSet, alreadyAssignedTargetsCount, assignedActions); } @@ -337,37 +356,31 @@ public class JpaDeploymentManagement implements DeploymentManagement { return distributionSet; } - private void checkQuotaForAssignment(final Collection targetsWithActionType, - final JpaDistributionSet distributionSet) { - // enforce the 'max targets per manual assignment' quota - if (!targetsWithActionType.isEmpty()) { - assertMaxTargetsPerManualAssignmentQuota(distributionSet.getId(), targetsWithActionType.size()); + private void checkQuotaForAssignment(final Collection deploymentRequests) { + if (!deploymentRequests.isEmpty()) { + enforceMaxAssignmentsPerRequest(deploymentRequests.size()); + enforceMaxActionsPerTarget(deploymentRequests); } } - /** - * Enforces the quota defining the maximum number of {@link Target}s per - * manual {@link DistributionSet} assignment. - * - * @param id - * of the distribution set - * @param requested - * number of targets to check - */ - private void assertMaxTargetsPerManualAssignmentQuota(final Long distributionSetId, - final int requestedTargetsCount) { - QuotaHelper.assertAssignmentQuota(distributionSetId, requestedTargetsCount, - quotaManagement.getMaxTargetsPerManualAssignment(), Target.class, DistributionSet.class, null); + private void enforceMaxAssignmentsPerRequest(final int requestedActions) { + QuotaHelper.assertAssignmentRequestSizeQuota(requestedActions, + quotaManagement.getMaxTargetDistributionSetAssignmentsPerManualAssignment()); + } + + private void enforceMaxActionsPerTarget(final Collection deploymentRequests) { + final int quota = quotaManagement.getMaxActionsPerTarget(); + + final Map countOfTargtInRequest = deploymentRequests.stream() + .map(DeploymentRequest::getControllerId) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + + countOfTargtInRequest.forEach((controllerId, count) -> QuotaHelper.assertAssignmentQuota(controllerId, count, + quota, Action.class, Target.class, actionRepository::countByTargetControllerId)); } private void closeOrCancelActiveActions(final AbstractDsAssignmentStrategy assignmentStrategy, final List> targetIdsChunks) { - - if (isMultiAssignmentsEnabled()) { - LOG.debug("Multi Assignments feature is enabled: No need to close /cancel active actions."); - return; - } - if (isActionsAutocloseEnabled()) { assignmentStrategy.closeActiveActions(targetIdsChunks); } else { @@ -396,10 +409,8 @@ public class JpaDeploymentManagement implements DeploymentManagement { private List createActions(final Collection targetsWithActionType, final List targets, final AbstractDsAssignmentStrategy assignmentStrategy, final JpaDistributionSet set) { - final Map targetsWithActionMap = targetsWithActionType.stream() - .collect(Collectors.toMap(TargetWithActionType::getControllerId, Function.identity())); - return targets.stream().map(trg -> assignmentStrategy.createTargetAction(targetsWithActionMap, trg, set)) + return targetsWithActionType.stream().map(twt -> assignmentStrategy.createTargetAction(twt, targets, set)) .filter(Objects::nonNull).map(actionRepository::save).collect(Collectors.toList()); } @@ -820,4 +831,17 @@ public class JpaDeploymentManagement implements DeploymentManagement { .runAsSystem(() -> tenantConfigurationManagement.getConfigurationValue(key, valueType).getValue()); } + private static RetryTemplate createRetryTemplate() { + final RetryTemplate template = new RetryTemplate(); + + final FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); + backOffPolicy.setBackOffPeriod(Constants.TX_RT_DELAY); + template.setBackOffPolicy(backOffPolicy); + + final SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(Constants.TX_RT_MAX, + Collections.singletonMap(ConcurrencyFailureException.class, true)); + template.setRetryPolicy(retryPolicy); + + return template; + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OfflineDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OfflineDsAssignmentStrategy.java index 89ae03eb6..f444c92fb 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OfflineDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OfflineDsAssignmentStrategy.java @@ -11,8 +11,9 @@ package org.eclipse.hawkbit.repository.jpa; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Set; +import java.util.function.BooleanSupplier; +import java.util.function.Function; import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.QuotaManagement; @@ -44,9 +45,9 @@ public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { OfflineDsAssignmentStrategy(final TargetRepository targetRepository, final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder, final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository, - final QuotaManagement quotaManagement) { + final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig) { super(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository, - quotaManagement); + quotaManagement, multiAssignmentsConfig); } @Override @@ -59,10 +60,15 @@ public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { @Override public List findTargetsForAssignment(final List controllerIDs, final long setId) { - return Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream() - .map(ids -> targetRepository.findAll(SpecificationsBuilder.combineWithAnd( - Arrays.asList(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, setId), - TargetSpecifications.notEqualToTargetUpdateStatus(TargetUpdateStatus.PENDING))))) + final Function, List> mapper; + if (isMultiAssignmentsEnabled()) { + mapper = targetRepository::findAllByControllerId; + } else { + mapper = ids -> targetRepository.findAll(SpecificationsBuilder.combineWithAnd( + Arrays.asList(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, setId), + TargetSpecifications.notEqualToTargetUpdateStatus(TargetUpdateStatus.PENDING)))); + } + return Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream().map(mapper) .flatMap(List::stream).collect(Collectors.toList()); } @@ -84,9 +90,9 @@ public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { } @Override - protected JpaAction createTargetAction(final Map targetsWithActionMap, - final JpaTarget target, final JpaDistributionSet set) { - final JpaAction result = super.createTargetAction(targetsWithActionMap, target, set); + protected JpaAction createTargetAction(final TargetWithActionType targetWithActionType, + final List targets, final JpaDistributionSet set) { + final JpaAction result = super.createTargetAction(targetWithActionType, targets, set); if (result != null) { result.setStatus(Status.FINISHED); result.setActive(Boolean.FALSE); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OnlineDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OnlineDsAssignmentStrategy.java index 30d0c893e..9086aeca3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OnlineDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OnlineDsAssignmentStrategy.java @@ -11,7 +11,6 @@ package org.eclipse.hawkbit.repository.jpa; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.function.BooleanSupplier; import java.util.function.Function; @@ -46,15 +45,12 @@ import com.google.common.collect.Lists; */ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { - private final BooleanSupplier multiAssignmentsConfig; - OnlineDsAssignmentStrategy(final TargetRepository targetRepository, final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder, final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig) { super(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository, - quotaManagement); - this.multiAssignmentsConfig = multiAssignmentsConfig; + quotaManagement, multiAssignmentsConfig); } @Override @@ -130,9 +126,9 @@ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { } @Override - JpaAction createTargetAction(final Map targetsWithActionMap, final JpaTarget target, + JpaAction createTargetAction(final TargetWithActionType targetWithActionType, final List targets, final JpaDistributionSet set) { - final JpaAction result = super.createTargetAction(targetsWithActionMap, target, set); + final JpaAction result = super.createTargetAction(targetWithActionType, targets, set); if (result != null) { result.setStatus(Status.RUNNING); } @@ -207,10 +203,6 @@ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { .publishEvent(new MultiActionEvent(tenant, eventPublisherHolder.getApplicationId(), controllerIds))); } - private boolean isMultiAssignmentsEnabled() { - return multiAssignmentsConfig.getAsBoolean(); - } - private static Stream filterCancellations(final List actions) { return actions.stream().filter(action -> { final Status actionStatus = action.getStatus(); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java index 6f98f0460..8364dc001 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 @@ -19,11 +19,10 @@ import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper; import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DistributionSet; -import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; -import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -143,11 +142,11 @@ public class AutoAssignChecker { return DeploymentHelper.runInNewTransaction(transactionManager, "autoAssignDSToTargets", Isolation.READ_COMMITTED.value(), status -> { - final List targets = getTargetsWithActionType(targetFilterQuery.getQuery(), - dsId, targetFilterQuery.getAutoAssignActionType(), PAGE_SIZE); - final int count = targets.size(); + final List deploymentRequests = createAssignmentRequests( + targetFilterQuery.getQuery(), dsId, targetFilterQuery.getAutoAssignActionType(), PAGE_SIZE); + final int count = deploymentRequests.size(); if (count > 0) { - deploymentManagement.assignDistributionSet(dsId, targets, actionMessage); + deploymentManagement.assignDistributionSets(deploymentRequests, actionMessage); } return count; }); @@ -168,7 +167,7 @@ public class AutoAssignChecker { * maximum amount of targets to retrieve * @return list of targets with action type */ - private List getTargetsWithActionType(final String targetFilterQuery, final Long dsId, + private List createAssignmentRequests(final String targetFilterQuery, final Long dsId, final ActionType type, final int count) { final Page targets = targetManagement.findByTargetFilterQueryAndNonDS(PageRequest.of(0, count), dsId, targetFilterQuery); @@ -176,8 +175,10 @@ public class AutoAssignChecker { // specified) final ActionType autoAssignActionType = type == null ? ActionType.FORCED : type; - return targets.getContent().stream().map(t -> new TargetWithActionType(t.getControllerId(), - autoAssignActionType, RepositoryModelConstants.NO_FORCE_TIME)).collect(Collectors.toList()); + return targets.getContent().stream() + .map(t -> DeploymentManagement.deploymentRequest(t.getControllerId(), dsId) + .setActionType(autoAssignActionType).build()) + .collect(Collectors.toList()); } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/QuotaHelper.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/QuotaHelper.java index e7c1b1ba1..f078f7f45 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/QuotaHelper.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/QuotaHelper.java @@ -8,7 +8,7 @@ */ package org.eclipse.hawkbit.repository.jpa.utils; -import java.util.function.Function; +import java.util.function.ToLongFunction; import javax.validation.constraints.NotNull; @@ -76,8 +76,8 @@ public final class QuotaHelper { * if the assignment operation would cause the quota to be * exceeded */ - public static void assertAssignmentQuota(final Long parentId, final long requested, final long limit, - @NotNull final Class type, @NotNull final Class parentType, final Function countFct) { + public static void assertAssignmentQuota(final T parentId, final long requested, final long limit, + @NotNull final Class type, @NotNull final Class parentType, final ToLongFunction countFct) { assertAssignmentQuota(parentId, requested, limit, type.getSimpleName(), parentType.getSimpleName(), countFct); } @@ -104,8 +104,8 @@ public final class QuotaHelper { * if the assignment operation would cause the quota to be * exceeded */ - public static void assertAssignmentQuota(final Long parentId, final long requested, final long limit, - @NotNull final String type, @NotNull final String parentType, final Function countFct) { + public static void assertAssignmentQuota(final T parentId, final long requested, final long limit, + @NotNull final String type, @NotNull final String parentType, final ToLongFunction countFct) { // check if the quota is unlimited if (limit <= 0) { @@ -121,7 +121,7 @@ public final class QuotaHelper { } if (parentId != null && countFct != null) { - final long currentCount = countFct.apply(parentId); + final long currentCount = countFct.applyAsLong(parentId); if (currentCount + requested > limit) { LOG.warn( "Cannot assign {} {} entities to {} '{}' because of the configured quota limit {}. Currently, there are {} {} entities assigned.", @@ -130,4 +130,22 @@ public final class QuotaHelper { } } } + + /** + * Assert that the number of assignments in a request does not exceed the + * limit. + * + * @param requested + * the number of assignments that are to be made + * @param limit + * the maximum number of assignments per request + */ + public static void assertAssignmentRequestSizeQuota(final long requested, final long limit) { + if (requested > limit) { + final String message = String.format( + "Quota exceeded: Cannot assign %s entities at once. The maximum is %s.", requested, limit); + LOG.warn(message); + throw new QuotaExceededException(message); + } + } } 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 5ff3f924a..324cafd03 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 @@ -55,7 +55,6 @@ import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; 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.ActionStatus; import org.eclipse.hawkbit.repository.model.Artifact; @@ -1328,8 +1327,8 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { final String knownExternalref = "externalRefId" + i; testdataFactory.createTarget(knownControllerId); - final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet( - knownDistributionSet.getId(), ActionType.FORCED, 0, Collections.singleton(knownControllerId)); + final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(knownDistributionSet.getId(), + knownControllerId); final Long actionId = getFirstAssignedActionId(assignmentResult); controllerManagement.updateActionExternalRef(actionId, knownExternalref); 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 f4fc40318..b0e8eb169 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 @@ -13,15 +13,18 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map.Entry; import java.util.stream.Collectors; -import java.util.stream.IntStream; +import org.assertj.core.api.Assertions; import org.eclipse.hawkbit.repository.ActionStatusFields; +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.entity.ActionCreatedEvent; @@ -35,6 +38,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.ForceQuitActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; @@ -47,13 +51,13 @@ 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.ActionStatus; +import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.DistributionSetTag; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; -import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; @@ -100,12 +104,7 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { final Target target = testdataFactory.createTarget(); final String dsName = "DistributionSet"; - verifyThrownExceptionBy(() -> deploymentManagement.assignDistributionSet(NOT_EXIST_IDL, - Collections.singletonList(new TargetWithActionType(target.getControllerId()))), dsName); - verifyThrownExceptionBy(() -> deploymentManagement.assignDistributionSet(NOT_EXIST_IDL, - Collections.singletonList(new TargetWithActionType(target.getControllerId())), "xxx"), dsName); - verifyThrownExceptionBy(() -> deploymentManagement.assignDistributionSet(NOT_EXIST_IDL, ActionType.FORCED, - System.currentTimeMillis(), Collections.singletonList(target.getControllerId())), dsName); + verifyThrownExceptionBy(() -> assignDistributionSet(NOT_EXIST_IDL, target.getControllerId()), dsName); verifyThrownExceptionBy(() -> deploymentManagement.cancelAction(NOT_EXIST_IDL), "Action"); verifyThrownExceptionBy(() -> deploymentManagement.countActionsByTarget(NOT_EXIST_ID), "Target"); @@ -158,34 +157,35 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Test verifies that the 'max actions per target' quota is enforced if the assigned distribution set is changed permanently.") - public void changeDistributionSetAssignmentUntilMaxActionsPerTargetQuotaIsExceeded() { + @Description("Test verifies that the 'max actions per target' quota is enforced.") + public void assertMaxActionsPerTargetQuotaIsEnforced() { final int maxActions = quotaManagement.getMaxActionsPerTarget(); - final List testTargets = testdataFactory.createTargets(1); + final Target testTarget = testdataFactory.createTarget(); final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); - final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); - final DistributionSet ds3 = testdataFactory.createDistributionSet("ds3"); - IntStream.range(0, maxActions).forEach(i -> { - assignDistributionSet(i % 2 == 0 ? ds1 : ds2, testTargets); - }); + enableMultiAssignments(); + for (int i = 0; i < maxActions; i++) { + deploymentManagement.offlineAssignedDistributionSets(Collections + .singletonList(new SimpleEntry(testTarget.getControllerId(), ds1.getId()))); + } - // change the distribution set one last time to trigger a quota hit assertThatExceptionOfType(QuotaExceededException.class) - .isThrownBy(() -> assignDistributionSet(ds3, testTargets)); + .isThrownBy(() -> assignDistributionSet(ds1, Collections.singletonList(testTarget))); } @Test - @Description("Assigns the same distribution set to many targets until the 'max targets per manual assignment' quota is exceeded.") - public void assignDistributionSetUntilQuotaIsExceeded() { + @Description("An assignment request with more assignments than allowed by 'maxTargetDistributionSetAssignmentsPerManualAssignment' quota throws an exception.") + public void assignmentRequestThatIsTooLarge() { + final int maxActions = quotaManagement.getMaxTargetDistributionSetAssignmentsPerManualAssignment(); + final DistributionSet ds1 = testdataFactory.createDistributionSet("1"); + final DistributionSet ds2 = testdataFactory.createDistributionSet("2"); - final int maxTargets = quotaManagement.getMaxTargetsPerManualAssignment(); - final DistributionSet ds = testdataFactory.createDistributionSet(); + final List targets = testdataFactory.createTargets(maxActions, "assignmentTest1"); + assignDistributionSet(ds1, targets); - assignDistributionSet(ds, testdataFactory.createTargets(maxTargets, "ok")); - assertThatExceptionOfType(QuotaExceededException.class) - .isThrownBy(() -> assignDistributionSet(ds, testdataFactory.createTargets(maxTargets + 1, "fail"))); + targets.add(testdataFactory.createTarget("assignmentTest2")); + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> assignDistributionSet(ds2, targets)); } @Test @@ -455,6 +455,7 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { @Expect(type = SoftwareModuleCreatedEvent.class, count = 6), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1) }) public void assignedDistributionSet() { + final List controllerIds = testdataFactory.createTargets(10).stream().map(Target::getControllerId) .collect(Collectors.toList()); final List onlineAssignedTargets = testdataFactory.createTargets(10, "2"); @@ -464,8 +465,15 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { assignDistributionSet(testdataFactory.createDistributionSet("2"), onlineAssignedTargets); final long current = System.currentTimeMillis(); - final List targets = deploymentManagement.offlineAssignedDistributionSet(ds.getId(), controllerIds) - .getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());; + + final List> offlineAssignments = controllerIds.stream() + .map(targetId -> new SimpleEntry(targetId, ds.getId())).collect(Collectors.toList()); + final List assignmentResults = deploymentManagement + .offlineAssignedDistributionSets(offlineAssignments); + assertThat(assignmentResults).hasSize(1); + final List targets = assignmentResults.get(0).getAssignedEntity().stream().map(Action::getTarget) + .collect(Collectors.toList()); + assertThat(actionRepository.count()).isEqualTo(20); assertThat(actionRepository.findByDistributionSetId(PAGE, ds.getId())).as("Offline actions are not active") .allMatch(action -> !action.isActive()); @@ -480,6 +488,33 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { .allMatch(target -> target.getLastModifiedAt() == target.getInstallationDate()); } + @Test + @Description("Offline assign multiple DSs to multiple Targets in multiassignment mode.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 4), @Expect(type = ActionCreatedEvent.class, count = 4), + @Expect(type = DistributionSetCreatedEvent.class, count = 2), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6) }) + public void multiOfflineAssignment() { + final List targetIds = testdataFactory.createTargets(2).stream().map(Target::getControllerId) + .collect(Collectors.toList()); + final List dsIds = testdataFactory.createDistributionSets(2).stream().map(DistributionSet::getId) + .collect(Collectors.toList()); + + enableMultiAssignments(); + final List> offlineAssignments = new ArrayList<>(); + targetIds.forEach(targetId -> dsIds + .forEach(dsId -> offlineAssignments.add(new SimpleEntry(targetId, dsId)))); + final List assignmentResults = deploymentManagement + .offlineAssignedDistributionSets(offlineAssignments); + + assertThat(getResultingActionCount(assignmentResults)).isEqualTo(4); + targetIds.forEach(controllerId -> { + final List assignedDsIds = actionRepository.findByTargetControllerId(PAGE, controllerId).stream() + .map(action -> action.getDistributionSet().getId()).collect(Collectors.toList()); + assertThat(assignedDsIds).containsExactlyInAnyOrderElementsOf(dsIds); + }); + } + @Test @Description("Verifies that if an account is set to action autoclose running actions in case of a new assigned set get closed and set to CANCELED.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 10), @@ -555,6 +590,92 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { assignment.stream().mapToLong(action -> action.getTarget().getId()).toArray()); } + @Test + @Description("Assign multiple DSs to multiple Targets in one request in multiassignment mode.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 4), @Expect(type = ActionCreatedEvent.class, count = 4), + @Expect(type = DistributionSetCreatedEvent.class, count = 2), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6), + @Expect(type = MultiActionEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 0) }) + public void multiassignmentInOneRequest() { + final List targets = testdataFactory.createTargets(2); + final List distributionSets = testdataFactory.createDistributionSets(2); + final List deploymentRequests = createAssignmentRequests(distributionSets, targets); + + enableMultiAssignments(); + final List results = deploymentManagement + .assignDistributionSets(deploymentRequests); + + assertThat(getResultingActionCount(results)).isEqualTo(deploymentRequests.size()); + final List dsIds = distributionSets.stream().map(DistributionSet::getId).collect(Collectors.toList()); + targets.forEach(target -> { + final List assignedDsIds = actionRepository.findByTargetControllerId(PAGE, target.getControllerId()) + .stream().map(action -> action.getDistributionSet().getId()).collect(Collectors.toList()); + assertThat(assignedDsIds).containsExactlyInAnyOrderElementsOf(dsIds); + }); + + } + + @Test + @Description("A Request resulting in multiple assignments to a single target is only allowed when multiassignment is enabled.") + public void multipleAssignmentsToTargetOnlyAllowedInMultiAssignMode() { + final Target target = testdataFactory.createTarget(); + final List distributionSets = testdataFactory.createDistributionSets(2); + + final DeploymentRequest targetToDS0 = DeploymentManagement + .deploymentRequest(target.getControllerId(), distributionSets.get(0).getId()).build(); + final DeploymentRequest targetToDS1 = DeploymentManagement + .deploymentRequest(target.getControllerId(), distributionSets.get(1).getId()).build(); + + Assertions.assertThatExceptionOfType(MultiAssignmentIsNotEnabledException.class) + .isThrownBy(() -> deploymentManagement.assignDistributionSets(Arrays.asList(targetToDS0, targetToDS1))); + + enableMultiAssignments(); + assertThat(getResultingActionCount( + deploymentManagement.assignDistributionSets(Arrays.asList(targetToDS0, targetToDS1)))).isEqualTo(2); + } + + @Test + @Description("Duplicate Assignments are removed from a request when multiassignment is disabled, otherwise not") + public void duplicateAssignmentsInRequestAreOnlyRemovedIfMultiassignmentDisabled() { + final Target target = testdataFactory.createTarget(); + final DistributionSet ds = testdataFactory.createDistributionSet(); + final List twoEqualAssignments = Collections.nCopies(2, + DeploymentManagement.deploymentRequest(target.getControllerId(), ds.getId()).build()); + + assertThat(getResultingActionCount(deploymentManagement.assignDistributionSets(twoEqualAssignments))) + .isEqualTo(1); + + enableMultiAssignments(); + assertThat(getResultingActionCount(deploymentManagement.assignDistributionSets(twoEqualAssignments))) + .isEqualTo(2); + } + + private int getResultingActionCount(final List results) { + return results.stream().map(DistributionSetAssignmentResult::getTotal).reduce(0, Integer::sum); + } + + @Test + @Description("An assignment request is not accepted if it would lead to a target exceeding the max actions per target quota.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 0) }) + public void maxActionsPerTargetIsCheckedBeforeAssignmentExecution() { + final int maxActions = quotaManagement.getMaxActionsPerTarget(); + final String controllerId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + + final List deploymentRequests = Collections.nCopies(maxActions + 1, + DeploymentManagement.deploymentRequest(controllerId, dsId).build()); + + enableMultiAssignments(); + Assertions.assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> deploymentManagement.assignDistributionSets(deploymentRequests)); + assertThat(actionRepository.countByTargetControllerId(controllerId)).isEqualTo(0); + } + /** * test a simple deployment by calling the * {@link TargetRepository#assignDistributionSet(DistributionSet, Iterable)} @@ -563,11 +684,11 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { */ @Test @Description("Simple deployment or distribution set to target assignment test.") - @ExpectEvents({@Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @ExpectEvents({ @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = TargetCreatedEvent.class, count = 30), @Expect(type = ActionCreatedEvent.class, count = 20), @Expect(type = TargetUpdatedEvent.class, count = 20), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) public void assignDistributionSet2Targets() { final String myCtrlIDPref = "myCtrlID"; @@ -617,7 +738,7 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Test that it is not possible to assign a distribution set that is not complete.") - @ExpectEvents({@Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @ExpectEvents({ @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = TargetCreatedEvent.class, count = 10), @Expect(type = ActionCreatedEvent.class, count = 10), @Expect(type = TargetUpdatedEvent.class, count = 10), @@ -650,14 +771,14 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Multiple deployments or distribution set to target assignment test. Expected behaviour is that a new deployment " + "overides unfinished old one which are canceled as part of the operation.") - @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 5 + 4), + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 5 + 4), @Expect(type = TargetUpdatedEvent.class, count = 3 * 4), @Expect(type = ActionCreatedEvent.class, count = 3 * 4), @Expect(type = ActionUpdatedEvent.class, count = 4 * 2), @Expect(type = CancelTargetAssignmentEvent.class, count = 4 * 2), @Expect(type = DistributionSetCreatedEvent.class, count = 3), @Expect(type = SoftwareModuleCreatedEvent.class, count = 9), - @Expect(type = TargetAssignDistributionSetEvent.class, count = 2)}) + @Expect(type = TargetAssignDistributionSetEvent.class, count = 2) }) public void mutipleDeployments() throws InterruptedException { final String undeployedTargetPrefix = "undep-T"; final int noOfUndeployedTargets = 5; @@ -982,10 +1103,8 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { final Target target = testdataFactory.createTarget("knownControllerId"); final DistributionSet ds = testdataFactory.createDistributionSet("a"); // assign ds to create an action - final DistributionSetAssignmentResult assignDistributionSet = deploymentManagement.assignDistributionSet( - ds.getId(), ActionType.SOFT, - org.eclipse.hawkbit.repository.model.RepositoryModelConstants.NO_FORCE_TIME, - Collections.singletonList(target.getControllerId())); + final DistributionSetAssignmentResult assignDistributionSet = assignDistributionSet(ds.getId(), + target.getControllerId(), ActionType.SOFT); final Long actionId = getFirstAssignedActionId(assignDistributionSet); // verify preparation Action findAction = deploymentManagement.findAction(actionId).get(); @@ -1006,10 +1125,8 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { final Target target = testdataFactory.createTarget("knownControllerId"); final DistributionSet ds = testdataFactory.createDistributionSet("a"); // assign ds to create an action - final DistributionSetAssignmentResult assignDistributionSet = deploymentManagement.assignDistributionSet( - ds.getId(), ActionType.FORCED, - org.eclipse.hawkbit.repository.model.RepositoryModelConstants.NO_FORCE_TIME, - Collections.singletonList(target.getControllerId())); + final DistributionSetAssignmentResult assignDistributionSet = assignDistributionSet(ds.getId(), + target.getControllerId(), ActionType.FORCED); final Long actionId = getFirstAssignedActionId(assignDistributionSet); // verify perparation Action findAction = deploymentManagement.findAction(actionId).get(); @@ -1023,23 +1140,22 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { findAction = deploymentManagement.findAction(actionId).get(); assertThat(findAction.getActionType()).as("action type is wrong").isEqualTo(ActionType.FORCED); } - + @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) final Action action = prepareFinishedUpdate("target1", "ds", false); - final Target target2 = testdataFactory.createTarget("target2"); + 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) assignDistributionSet(action.getDistributionSet().getId(), target2.getControllerId()); - final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet( + final DistributionSetAssignmentResult assignmentResult = assignDistributionSet( action.getDistributionSet().getId(), - Arrays.asList(new TargetWithActionType(action.getTarget().getControllerId()), - new TargetWithActionType(target3.getControllerId()))); - + Arrays.asList(action.getTarget().getControllerId(), target3.getControllerId()), ActionType.FORCED); + assertThat(assignmentResult).isNotNull(); assertThat(assignmentResult.getTotal()).as("Total count of assigned and already assigned targets").isEqualTo(2); assertThat(assignmentResult.getAssigned()).as("Total count of assigned targets").isEqualTo(1); @@ -1170,8 +1286,4 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { } - private void enableMultiAssignments() { - tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, true); - } - } 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 affedb17c..8692de35f 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 @@ -13,7 +13,6 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -102,8 +101,8 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { final String knownControllerId = "controller12345"; final DistributionSet knownDistributionSet = testdataFactory.createDistributionSet(); testdataFactory.createTarget(knownControllerId); - final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet( - knownDistributionSet.getId(), ActionType.FORCED, 0, Collections.singleton(knownControllerId)); + final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(knownDistributionSet.getId(), + knownControllerId); final Long manuallyAssignedActionId = getFirstAssignedActionId(assignmentResult); // create rollout with the same distribution set already assigned @@ -142,8 +141,8 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { final DistributionSet firstDistributionSet = testdataFactory.createDistributionSet(); final DistributionSet secondDistributionSet = testdataFactory.createDistributionSet("second"); testdataFactory.createTarget(knownControllerId); - final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet( - firstDistributionSet.getId(), ActionType.FORCED, 0, Collections.singleton(knownControllerId)); + final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(firstDistributionSet.getId(), + knownControllerId); final Long manuallyAssignedActionId = getFirstAssignedActionId(assignmentResult); // create rollout with the same distribution set already assigned 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 5a0b25f91..207763790 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 @@ -11,7 +11,6 @@ package org.eclipse.hawkbit.repository.jpa.autoassign; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -57,8 +56,8 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest { final DistributionSet firstDistributionSet = testdataFactory.createDistributionSet(); final DistributionSet secondDistributionSet = testdataFactory.createDistributionSet("second"); testdataFactory.createTarget(knownControllerId); - final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet( - firstDistributionSet.getId(), ActionType.FORCED, 0, Collections.singleton(knownControllerId)); + final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(firstDistributionSet.getId(), + knownControllerId); final Long manuallyAssignedActionId = getFirstAssignedActionId(assignmentResult); // target filter query that matches all targets @@ -218,11 +217,11 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest { final String targetDsBIdPref = "B"; final String targetDsCIdPref = "C"; - List targetsA = createTargetsAndAutoAssignDistSet(targetDsAIdPref, 5, distributionSet, + final List targetsA = createTargetsAndAutoAssignDistSet(targetDsAIdPref, 5, distributionSet, ActionType.FORCED); - List targetsB = createTargetsAndAutoAssignDistSet(targetDsBIdPref, 10, distributionSet, + final List targetsB = createTargetsAndAutoAssignDistSet(targetDsBIdPref, 10, distributionSet, ActionType.SOFT); - List targetsC = createTargetsAndAutoAssignDistSet(targetDsCIdPref, 10, distributionSet, + final List targetsC = createTargetsAndAutoAssignDistSet(targetDsCIdPref, 10, distributionSet, ActionType.DOWNLOAD_ONLY); final int targetsCount = targetsA.size() + targetsB.size() + targetsC.size(); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanupTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanupTest.java index 4f150e0ed..5c16f6fa3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanupTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanupTest.java @@ -14,12 +14,10 @@ import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationPrope import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.ACTION_CLEANUP_ENABLED; import java.util.Arrays; -import java.util.Collections; import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; 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.DistributionSet; import org.eclipse.hawkbit.repository.model.Target; @@ -54,10 +52,8 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest { final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); - deploymentManagement.assignDistributionSet(ds1.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg1.getControllerId())); - deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg2.getControllerId())); + assignDistributionSet(ds1.getId(), trg1.getControllerId()); + assignDistributionSet(ds2.getId(), trg2.getControllerId()); assertThat(actionRepository.count()).isEqualTo(2); @@ -80,10 +76,8 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest { final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); - final Long action1 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds1.getId(), - ActionType.FORCED, 0, Collections.singletonList(trg1.getControllerId()))); - deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg2.getControllerId())); + final Long action1 = getFirstAssignedActionId(assignDistributionSet(ds1.getId(), trg1.getControllerId())); + assignDistributionSet(ds2.getId(), trg2.getControllerId()); setActionToCanceled(action1); @@ -109,12 +103,9 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest { final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); - final Long action1 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds1.getId(), - ActionType.FORCED, 0, Collections.singletonList(trg1.getControllerId()))); - final Long action2 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds2.getId(), - ActionType.FORCED, 0, Collections.singletonList(trg2.getControllerId()))); - final Long action3 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds2.getId(), - ActionType.FORCED, 0, Collections.singletonList(trg3.getControllerId()))); + final Long action1 = getFirstAssignedActionId(assignDistributionSet(ds1.getId(), trg1.getControllerId())); + final Long action2 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), trg2.getControllerId())); + final Long action3 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), trg3.getControllerId())); assertThat(actionRepository.count()).isEqualTo(3); @@ -144,12 +135,9 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest { final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); - final Long action1 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds1.getId(), - ActionType.FORCED, 0, Collections.singletonList(trg1.getControllerId()))); - final Long action2 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds2.getId(), - ActionType.FORCED, 0, Collections.singletonList(trg2.getControllerId()))); - final Long action3 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds2.getId(), - ActionType.FORCED, 0, Collections.singletonList(trg3.getControllerId()))); + final Long action1 = getFirstAssignedActionId(assignDistributionSet(ds1.getId(), trg1.getControllerId())); + final Long action2 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), trg2.getControllerId())); + final Long action3 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), trg3.getControllerId())); assertThat(actionRepository.count()).isEqualTo(3); @@ -181,12 +169,9 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest { final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); - final Long action1 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds1.getId(), - ActionType.FORCED, 0, Collections.singletonList(trg1.getControllerId()))); - final Long action2 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds2.getId(), - ActionType.FORCED, 0, Collections.singletonList(trg2.getControllerId()))); - final Long action3 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds2.getId(), - ActionType.FORCED, 0, Collections.singletonList(trg3.getControllerId()))); + final Long action1 = getFirstAssignedActionId(assignDistributionSet(ds1.getId(), trg1.getControllerId())); + final Long action2 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), trg2.getControllerId())); + final Long action3 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), trg3.getControllerId())); assertThat(actionRepository.count()).isEqualTo(3); 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 a1fc94d11..4b02fcc99 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 @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.repository.test.util; +import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE; import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.SYSTEM_ROLE; @@ -15,7 +16,9 @@ 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; @@ -46,6 +49,7 @@ import org.eclipse.hawkbit.repository.TargetTagManagement; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; @@ -55,11 +59,11 @@ import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetMetadata; -import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.repository.test.TestConfiguration; import org.eclipse.hawkbit.repository.test.matcher.EventVerifier; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -231,8 +235,42 @@ public abstract class AbstractIntegrationTest { protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final String controllerId, final ActionType actionType) { - return deploymentManagement.assignDistributionSet(dsID, Collections.singletonList( - new TargetWithActionType(controllerId, actionType, RepositoryModelConstants.NO_FORCE_TIME))); + return assignDistributionSet(dsID, Collections.singletonList(controllerId), actionType); + } + + protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final String controllerId, + final ActionType actionType, final long forcedTime) { + return assignDistributionSet(dsID, Collections.singletonList(controllerId), actionType, forcedTime); + } + + protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final List controllerIds, + final ActionType actionType) { + return assignDistributionSet(dsID, controllerIds, actionType, RepositoryModelConstants.NO_FORCE_TIME); + } + + protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final List controllerIds, + final ActionType actionType, final long forcedTime) { + final List deploymentRequests = controllerIds.stream() + .map(id -> DeploymentManagement.deploymentRequest(id, dsID).setActionType(actionType) + .setForceTime(forcedTime).build()) + .collect(Collectors.toList()); + final List results = deploymentManagement + .assignDistributionSets(deploymentRequests); + assertThat(results).hasSize(1); + return results.get(0); + } + + protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet ds, + final List targets) { + final List targetIds = targets.stream().map(Target::getControllerId).collect(Collectors.toList()); + return assignDistributionSet(ds.getId(), targetIds, ActionType.FORCED); + } + + private DistributionSetAssignmentResult makeAssignment(final DeploymentRequest request) { + final List results = deploymentManagement + .assignDistributionSets(Collections.singletonList(request)); + assertThat(results).hasSize(1); + return results.get(0); } /** @@ -264,34 +302,37 @@ public abstract class AbstractIntegrationTest { protected DistributionSetAssignmentResult assignDistributionSetWithMaintenanceWindow(final long dsID, final String controllerId, final String maintenanceWindowSchedule, final String maintenanceWindowDuration, final String maintenanceWindowTimeZone) { - return deploymentManagement.assignDistributionSet(dsID, - Arrays.asList(new TargetWithActionType(controllerId, ActionType.FORCED, - RepositoryModelConstants.NO_FORCE_TIME, maintenanceWindowSchedule, maintenanceWindowDuration, - maintenanceWindowTimeZone))); + + return makeAssignment(DeploymentManagement.deploymentRequest(controllerId, dsID) + .setMaintenance(maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone) + .build()); } - protected DistributionSetAssignmentResult assignDistributionSetWithMaintenanceWindowTimeForced(final long dsID, - final String controllerId, final String maintenanceWindowSchedule, final String maintenanceWindowDuration, + protected DistributionSetAssignmentResult assignDistributionSetWithMaintenanceWindow(final long dsID, + final String controllerId, final ActionType type, final String maintenanceWindowSchedule, + final String maintenanceWindowDuration, final String maintenanceWindowTimeZone) { - return deploymentManagement.assignDistributionSet(dsID, - Arrays.asList(new TargetWithActionType(controllerId, ActionType.TIMEFORCED, System.currentTimeMillis(), - maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone))); + return makeAssignment(DeploymentManagement.deploymentRequest(controllerId, dsID).setActionType(type) + .setMaintenance(maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone) + .build()); } - protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet pset, - final List targets) { - return deploymentManagement.assignDistributionSet(pset.getId(), - targets.stream().map(Target::getTargetWithActionType).collect(Collectors.toList())); + 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 assignDistributionSetTimeForced(final DistributionSet pset, - final Target target) { - return deploymentManagement.assignDistributionSet(pset.getId(), Arrays.asList( - new TargetWithActionType(target.getControllerId(), ActionType.TIMEFORCED, System.currentTimeMillis()))); + protected void enableMultiAssignments() { + tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, true); } protected DistributionSetMetadata createDistributionSetMetadata(final long dsId, final MetaData md) { diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java index 82e3d7e80..a54e912bf 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java @@ -42,7 +42,6 @@ import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.DistributionSet; -import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.test.matcher.Expect; @@ -154,10 +153,9 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { getOsModule(ds), "test1.signature", ARTIFACT_SIZE); final Target savedTarget = createTargetAndAssertNoActiveActions(); - final List targetsAssignedToDs = deploymentManagement - .assignDistributionSet(ds.getId(), ActionType.FORCED, RepositoryModelConstants.NO_FORCE_TIME, - Collections.singletonList(savedTarget.getControllerId())) - .getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());; + final List targetsAssignedToDs = assignDistributionSet(ds.getId(), savedTarget.getControllerId(), + ActionType.FORCED).getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList()); + assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1); final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) @@ -208,9 +206,8 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { final Target target = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID); final DistributionSet ds = testdataFactory.createDistributionSet("", true); - final Long actionId = getFirstAssignedActionId( - deploymentManagement.assignDistributionSet(ds.getId(), ActionType.TIMEFORCED, - System.currentTimeMillis() + 2_000, Collections.singletonList(target.getControllerId()))); + final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds.getId(), target.getControllerId(), + ActionType.TIMEFORCED, System.currentTimeMillis() + 2_000)); MvcResult mvcResult = performGet("/{tenant}/controller/v1/" + DEFAULT_CONTROLLER_ID, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant()).andReturn(); @@ -257,8 +254,7 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { final Target savedTarget = createTargetAndAssertNoActiveActions(); - final List saved = deploymentManagement.assignDistributionSet(ds.getId(), ActionType.SOFT, - RepositoryModelConstants.NO_FORCE_TIME, Collections.singletonList(savedTarget.getControllerId())) + final List saved = assignDistributionSet(ds.getId(), savedTarget.getControllerId(), ActionType.SOFT) .getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList()); assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1); @@ -317,8 +313,8 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { final Target savedTarget = createTargetAndAssertNoActiveActions(); - final List saved = deploymentManagement.assignDistributionSet(ds.getId(), ActionType.TIMEFORCED, - System.currentTimeMillis(), Collections.singletonList(savedTarget.getControllerId())) + final List saved = assignDistributionSet(ds.getId(), savedTarget.getControllerId(), + ActionType.TIMEFORCED) .getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList()); assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1); @@ -385,8 +381,8 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { final Target savedTarget = createTargetAndAssertNoActiveActions(); - final List saved = deploymentManagement.assignDistributionSet(ds.getId(), ActionType.DOWNLOAD_ONLY, - RepositoryModelConstants.NO_FORCE_TIME, Collections.singletonList(savedTarget.getControllerId())) + final List saved = assignDistributionSet(ds.getId(), savedTarget.getControllerId(), + ActionType.DOWNLOAD_ONLY) .getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList()); assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1); diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/MgmtId.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/MgmtId.java index e83c43d52..f87f36ca6 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/MgmtId.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/MgmtId.java @@ -8,8 +8,8 @@ */ package org.eclipse.hawkbit.mgmt.json.model; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; /** * A generic abstract rest model which contains only a ID for use-case e.g. @@ -19,12 +19,28 @@ import com.fasterxml.jackson.annotation.JsonProperty; */ @JsonIgnoreProperties(ignoreUnknown = true) public class MgmtId { - - @JsonProperty private Long id; + + /** + * Constructor + */ + public MgmtId() { + } /** - * @return the id + * Constructor + * + * @param id + * ID of object + */ + @JsonCreator + public MgmtId(final Long id) { + this.id = id; + } + + + /** + * @return the ID */ public Long getId() { return id; @@ -32,7 +48,7 @@ public class MgmtId { /** * @param id - * the id to set + * the ID to set */ public void setId(final Long id) { this.id = id; 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 730b0f3be..c9cc2421b 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 @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.mgmt.json.model.distributionset; import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -20,18 +21,21 @@ import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public class MgmtTargetAssignmentRequestBody { - @JsonProperty private String id; - private long forcetime; - private MgmtActionType type; + private MgmtMaintenanceWindowRequestBody maintenanceWindow; /** - * {@link MgmtMaintenanceWindowRequestBody} object containing schedule, - * duration and timezone. + * JsonCreator Constructor + * + * @param id + * Mandatory ID of the target that should be assigned */ - private MgmtMaintenanceWindowRequestBody maintenanceWindow; + @JsonCreator + public MgmtTargetAssignmentRequestBody(@JsonProperty(required = true, value = "id") final String id) { + this.id = id; + } public String getId() { return id; diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremoduletype/MgmtSoftwareModuleTypeAssigment.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremoduletype/MgmtSoftwareModuleTypeAssigment.java index 43bdc157e..d590d9470 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremoduletype/MgmtSoftwareModuleTypeAssigment.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremoduletype/MgmtSoftwareModuleTypeAssigment.java @@ -18,5 +18,4 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; */ @JsonIgnoreProperties(ignoreUnknown = true) public class MgmtSoftwareModuleTypeAssigment extends MgmtId { - } 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 96ba199d6..85b0b1a97 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 @@ -7,19 +7,29 @@ import org.eclipse.hawkbit.mgmt.json.model.MgmtId; import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + /** * Request Body of DistributionSet for assignment operations (ID only). * */ public class MgmtDistributionSetAssignment extends MgmtId { + private long forcetime; private MgmtActionType type; + private MgmtMaintenanceWindowRequestBody maintenanceWindow; /** - * {@link MgmtMaintenanceWindowRequestBody} object defining a schedule, - * duration and timezone. + * Constructor + * + * @param id + * ID of object */ - private MgmtMaintenanceWindowRequestBody maintenanceWindow; + @JsonCreator + public MgmtDistributionSetAssignment(@JsonProperty(required = true, value = "id") final Long id) { + super(id); + } public MgmtActionType getType() { return type; diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignments.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignments.java new file mode 100644 index 000000000..d58f390ba --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignments.java @@ -0,0 +1,56 @@ +/** + * 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.mgmt.json.model.target; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * Class to hold multiple distribution set assignments. A JSON object + * representing a single {@link MgmtDistributionSetAssignment} can be + * deserialized to an object of this class. + */ +@JsonDeserialize(using = MgmtDistributionSetAssignmentsDeserializer.class) +public class MgmtDistributionSetAssignments extends ArrayList { + private static final long serialVersionUID = 1L; + + /** + * Constructor for an object that contains no distribution set assignment + * + */ + public MgmtDistributionSetAssignments() { + super(); + } + + /** + * Constructor for an object that contains a single distribution set + * assignment + * + * @param assignment + * the assignment + */ + public MgmtDistributionSetAssignments(final MgmtDistributionSetAssignment assignment) { + super(); + add(assignment); + } + + /** + * Constructor for an object that contains multiple distribution set + * assignments + * + * @param assignments + * the assignments + */ + public MgmtDistributionSetAssignments(final List assignments) { + super(assignments); + } + +} diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignmentsDeserializer.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignmentsDeserializer.java new file mode 100644 index 000000000..c9a6bda95 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignmentsDeserializer.java @@ -0,0 +1,51 @@ +/** + * 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.mgmt.json.model.target; + +import java.io.IOException; +import java.util.Arrays; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +/** + * Deserializes a single object or a List of + * {@link MgmtDistributionSetAssignment}s + */ +public class MgmtDistributionSetAssignmentsDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; + + /** + * Mandatory constructor + */ + public MgmtDistributionSetAssignmentsDeserializer() { + this(null); + } + + protected MgmtDistributionSetAssignmentsDeserializer(final Class vc) { + super(vc); + } + + @Override + public MgmtDistributionSetAssignments deserialize(final JsonParser jp, final DeserializationContext ctx) + throws IOException { + final MgmtDistributionSetAssignments assignments = new MgmtDistributionSetAssignments(); + final ObjectCodec codec = jp.getCodec(); + final JsonNode node = codec.readTree(jp); + if (node.isArray()) { + assignments.addAll(Arrays.asList(codec.treeToValue(node, MgmtDistributionSetAssignment[].class))); + } else { + assignments.add(codec.treeToValue(node, MgmtDistributionSetAssignment.class)); + } + return assignments; + } +} diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java index 37e47dff7..e51cfd19e 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java @@ -18,7 +18,7 @@ import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentResponseBody; -import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssignment; +import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssignments; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAttributes; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody; @@ -261,8 +261,8 @@ public interface MgmtTargetRestApi { * * @param targetId * of the target to change - * @param dsId - * of the distributionset that is to be assigned + * @param dsAssignments + * the requested Assignments that shall be made * @param offline * to true if update was executed offline, i.e. not * managed by hawkBit. @@ -275,7 +275,7 @@ public interface MgmtTargetRestApi { MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) ResponseEntity postAssignedDistributionSet( - @PathVariable("targetId") String targetId, MgmtDistributionSetAssignment dsId, + @PathVariable("targetId") String targetId, MgmtDistributionSetAssignments dsAssignments, @RequestParam(value = "offline", required = false) boolean offline); /** 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 new file mode 100644 index 000000000..73c545a24 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDeploymentRequestMapper.java @@ -0,0 +1,74 @@ +/** + * 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.mgmt.rest.resource; + +import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentRequestBody; +import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssignment; +import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; +import org.eclipse.hawkbit.repository.model.DeploymentRequest; +import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder; + +/** + * A mapper for assignment requests + */ +public final class MgmtDeploymentRequestMapper { + private MgmtDeploymentRequestMapper() { + // Utility class + } + + /** + * Convert assignment information to an {@link DeploymentRequest} + * + * @param dsAssignment + * DS assignment information + * @param targetId + * target to assign the DS to + * @return resulting {@link DeploymentRequest} + */ + public static DeploymentRequest createAssignmentRequest(final MgmtDistributionSetAssignment dsAssignment, + final String targetId) { + + return createAssignmentRequest(targetId, dsAssignment.getId(), dsAssignment.getType(), + dsAssignment.getForcetime(), dsAssignment.getMaintenanceWindow()); + } + + /** + * Convert assignment information to an {@link DeploymentRequest} + * + * @param targetAssignment + * target assignment information + * @param dsId + * DS to assign the target to + * @return resulting {@link DeploymentRequest} + */ + public static DeploymentRequest createAssignmentRequest(final MgmtTargetAssignmentRequestBody targetAssignment, + final Long dsId) { + + return createAssignmentRequest(targetAssignment.getId(), dsId, targetAssignment.getType(), + targetAssignment.getForcetime(), targetAssignment.getMaintenanceWindow()); + } + + private static DeploymentRequest createAssignmentRequest(final String targetId, final Long dsId, + final MgmtActionType type, final long forcetime, final MgmtMaintenanceWindowRequestBody maintenanceWindow) { + final DeploymentRequestBuilder request = DeploymentManagement.deploymentRequest(targetId, dsId) + .setActionType(MgmtRestModelMapper.convertActionType(type)).setForceTime(forcetime); + if (maintenanceWindow != null) { + final String cronSchedule = maintenanceWindow.getSchedule(); + final String duration = maintenanceWindow.getDuration(); + final String timezone = maintenanceWindow.getTimezone(); + MaintenanceScheduleHelper.validateMaintenanceSchedule(cronSchedule, duration, timezone); + request.setMaintenance(cronSchedule, duration, timezone); + } + return request.build(); + } + +} diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetMapper.java index 0156bca92..18127da8a 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetMapper.java @@ -144,6 +144,20 @@ public final class MgmtDistributionSetMapper { return result; } + static MgmtTargetAssignmentResponseBody toResponse( + final List dsAssignmentResults) { + final MgmtTargetAssignmentResponseBody result = new MgmtTargetAssignmentResponseBody(); + final int alreadyAssigned = dsAssignmentResults.stream() + .mapToInt(DistributionSetAssignmentResult::getAlreadyAssigned).sum(); + final List assignedActions = dsAssignmentResults.stream() + .flatMap(assignmentResult -> assignmentResult.getAssignedEntity().stream()) + .map(action -> new MgmtActionId(action.getTarget().getControllerId(), action.getId())) + .collect(Collectors.toList()); + result.setAlreadyAssigned(alreadyAssigned); + result.setAssignedActions(assignedActions); + return result; + } + static List toResponseDistributionSets(final Collection sets) { if (sets == null) { return Collections.emptyList(); 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 b99ec3ead..f61fd4c36 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 @@ -8,11 +8,12 @@ */ package org.eclipse.hawkbit.mgmt.rest.resource; +import java.util.AbstractMap.SimpleEntry; import java.util.Collection; import java.util.List; +import java.util.Map.Entry; import java.util.stream.Collectors; -import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody; import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata; import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut; import org.eclipse.hawkbit.mgmt.json.model.PagedList; @@ -30,20 +31,19 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.EntityFactory; -import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; -import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -250,33 +250,22 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { @PathVariable("distributionSetId") final Long distributionSetId, @RequestBody final List assignments, @RequestParam(value = "offline", required = false) final boolean offline) { - if (offline) { - return ResponseEntity.ok(MgmtDistributionSetMapper - .toResponse(this.deployManagament.offlineAssignedDistributionSet(distributionSetId, assignments - .stream().map(MgmtTargetAssignmentRequestBody::getId).collect(Collectors.toList())))); + final List> offlineAssignments = assignments.stream() + .map(assignment -> new SimpleEntry(assignment.getId(), distributionSetId)) + .collect(Collectors.toList()); + return ResponseEntity + .ok(MgmtDistributionSetMapper + .toResponse(deployManagament.offlineAssignedDistributionSets(offlineAssignments))); } + + final List deploymentRequests = assignments.stream() + .map(assignment -> MgmtDeploymentRequestMapper.createAssignmentRequest(assignment, distributionSetId)) + .collect(Collectors.toList()); - final DistributionSetAssignmentResult assignDistributionSet = this.deployManagament - .assignDistributionSet(distributionSetId, assignments.stream().map(t -> { - final MgmtMaintenanceWindowRequestBody maintenanceWindow = t.getMaintenanceWindow(); - - if (maintenanceWindow == null) { - return new TargetWithActionType(t.getId(), MgmtRestModelMapper.convertActionType(t.getType()), - t.getForcetime()); - } - - final String cronSchedule = maintenanceWindow.getSchedule(); - final String duration = maintenanceWindow.getDuration(); - final String timezone = maintenanceWindow.getTimezone(); - - MaintenanceScheduleHelper.validateMaintenanceSchedule(cronSchedule, duration, timezone); - - return new TargetWithActionType(t.getId(), MgmtRestModelMapper.convertActionType(t.getType()), - t.getForcetime(), cronSchedule, duration, timezone); - }).collect(Collectors.toList())); - - return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(assignDistributionSet)); + final List assignmentResults = deployManagament + .assignDistributionSets(deploymentRequests); + return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(assignmentResults)); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java index a91591bcc..cc750fe09 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java @@ -8,14 +8,16 @@ */ package org.eclipse.hawkbit.mgmt.rest.resource; +import java.util.AbstractMap.SimpleEntry; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import javax.validation.Valid; import javax.validation.ValidationException; -import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody; import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata; import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut; import org.eclipse.hawkbit.mgmt.json.model.PagedList; @@ -25,7 +27,7 @@ import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentResponseBody; -import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssignment; +import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssignments; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAttributes; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody; @@ -33,15 +35,15 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.EntityFactory; -import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.DeploymentRequest; +import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetMetadata; -import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -281,36 +283,25 @@ public class MgmtTargetResource implements MgmtTargetRestApi { @Override public ResponseEntity postAssignedDistributionSet( - @PathVariable("targetId") final String targetId, @RequestBody final MgmtDistributionSetAssignment dsId, + @PathVariable("targetId") final String targetId, + @Valid @RequestBody final MgmtDistributionSetAssignments dsAssignments, @RequestParam(value = "offline", required = false) final boolean offline) { - if (offline) { + final List> offlineAssignments = dsAssignments.stream() + .map(dsAssignment -> new SimpleEntry(targetId, dsAssignment.getId())) + .collect(Collectors.toList()); return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse( - deploymentManagement.offlineAssignedDistributionSet(dsId.getId(), - Collections.singletonList(targetId)))); + deploymentManagement.offlineAssignedDistributionSets(offlineAssignments))); } - findTargetWithExceptionIfNotFound(targetId); - final MgmtMaintenanceWindowRequestBody maintenanceWindow = dsId.getMaintenanceWindow(); - if (maintenanceWindow == null) { - return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(this.deploymentManagement - .assignDistributionSet(dsId.getId(), Collections.singletonList(new TargetWithActionType(targetId, - MgmtRestModelMapper.convertActionType(dsId.getType()), dsId.getForcetime()))))); - } - - final String cronSchedule = maintenanceWindow.getSchedule(); - final String duration = maintenanceWindow.getDuration(); - final String timezone = maintenanceWindow.getTimezone(); - - MaintenanceScheduleHelper.validateMaintenanceSchedule(cronSchedule, duration, timezone); - - return ResponseEntity - .ok(MgmtDistributionSetMapper.toResponse(this.deploymentManagement.assignDistributionSet(dsId.getId(), - Collections.singletonList(new TargetWithActionType(targetId, - MgmtRestModelMapper.convertActionType(dsId.getType()), dsId.getForcetime(), - cronSchedule, duration, timezone))))); + final List deploymentRequests = dsAssignments.stream() + .map(dsAssignment -> MgmtDeploymentRequestMapper.createAssignmentRequest(dsAssignment, targetId)) + .collect(Collectors.toList()); + final List assignmentResults = deploymentManagement + .assignDistributionSets(deploymentRequests); + return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(assignmentResults)); } @Override 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 d58f48144..3f0af689d 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 @@ -27,10 +27,12 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.commons.lang3.RandomStringUtils; 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.QuotaExceededException; import org.eclipse.hawkbit.repository.model.Action; @@ -40,7 +42,6 @@ import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; import org.eclipse.hawkbit.repository.model.NamedEntity; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; -import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.repository.test.util.TestdataFactory; import org.eclipse.hawkbit.repository.test.util.WithUser; import org.eclipse.hawkbit.rest.util.JsonBuilder; @@ -299,11 +300,10 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr } @Test - @Description("Ensures that multi target assignment is protected by our 'max targets per manual assignment' quota.") + @Description("Ensures that multi target assignment is protected by our getMaxTargetDistributionSetAssignmentsPerManualAssignment quota.") public void assignMultipleTargetsToDistributionSetUntilQuotaIsExceeded() throws Exception { - - final int maxTargets = quotaManagement.getMaxTargetsPerManualAssignment(); - final List targets = testdataFactory.createTargets(maxTargets + 1); + final int maxActions = quotaManagement.getMaxTargetDistributionSetAssignmentsPerManualAssignment(); + final List targets = testdataFactory.createTargets(maxActions + 1); final DistributionSet ds = testdataFactory.createDistributionSet(); final JSONArray payload = new JSONArray(); @@ -943,8 +943,7 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr assertThat(distributionSetManagement.findByCompleted(PAGE, true)).hasSize(0); final DistributionSet set = testdataFactory.createDistributionSet("one"); - deploymentManagement.assignDistributionSet(set.getId(), - Arrays.asList(new TargetWithActionType(testdataFactory.createTarget().getControllerId()))); + assignDistributionSet(set.getId(), testdataFactory.createTarget().getControllerId()); assertThat(distributionSetManagement.count()).isEqualTo(1); @@ -1273,4 +1272,63 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr .as("Five targets in repository have DS assigned").hasSize(5); } + @Test + @Description("A request for assigning a target multiple times results in a Bad Request when multiassignment is disabled.") + public void multiassignmentRequestNotAllowedIfDisabled() throws Exception { + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + + final JSONArray body = new JSONArray(); + body.put(getAssignmentObject(targetId, MgmtActionType.SOFT)); + body.put(getAssignmentObject(targetId, MgmtActionType.FORCED)); + + mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", dsId).content(body.toString()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + } + + @Test + @Description("Identical assignments in a single request are removed when multiassignment is disabled.") + public void identicalAssignmentInRequestAreRemovedIfMultiassignmentsDisabled() throws Exception { + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + + final JSONArray body = new JSONArray(); + body.put(getAssignmentObject(targetId, MgmtActionType.FORCED)); + body.put(getAssignmentObject(targetId, MgmtActionType.FORCED)); + + mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", dsId).content(body.toString()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("total", equalTo(1))); + } + + @Test + @Description("Assigning targets multiple times to a DS in one request works in multiassignment mode.") + public void multiAssignment() throws Exception { + final List targetIds = testdataFactory.createTargets(2).stream().map(Target::getControllerId) + .collect(Collectors.toList()); + 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)); + + enableMultiAssignments(); + mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", dsId).content(body.toString()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("total", equalTo(body.length()))); + } + + + + 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; + } + } 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 31f5ecab7..eafa344ea 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,6 +26,7 @@ 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; @@ -60,6 +61,7 @@ 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; @@ -1240,8 +1242,8 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest public void updateAction() throws Exception { final Target target = testdataFactory.createTarget(); final DistributionSet set = testdataFactory.createDistributionSet(); - final Long actionId = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(set.getId(), - ActionType.SOFT, 0, Collections.singletonList(target.getControllerId()))); + final Long actionId = getFirstAssignedActionId( + assignDistributionSet(set.getId(), target.getControllerId(), ActionType.SOFT)); assertThat(deploymentManagement.findAction(actionId).get().getActionType()).isEqualTo(ActionType.SOFT); final String body = new JSONObject().put("forceType", "forced").toString(); @@ -1293,7 +1295,7 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest @Description("Verfies that a DOWNLOAD_ONLY DS to target assignment is properly handled") public void assignDownloadOnlyDistributionSetToTarget() throws Exception { - Target target = testdataFactory.createTarget(); + final Target target = testdataFactory.createTarget(); final DistributionSet set = testdataFactory.createDistributionSet("one"); mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + "/assignedDS") @@ -1303,7 +1305,7 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest .andExpect(jsonPath("total", equalTo(1))); assertThat(deploymentManagement.getAssignedDistributionSet(target.getControllerId()).get()).isEqualTo(set); - Slice actions = deploymentManagement.findActionsByTarget("targetExist", PageRequest.of(0, 100)); + final Slice actions = deploymentManagement.findActionsByTarget("targetExist", PageRequest.of(0, 100)); assertThat(actions.getSize()).isGreaterThan(0); actions.stream().filter(a -> a.getDistributionSet().equals(set)) .forEach(a -> ActionType.DOWNLOAD_ONLY.equals(a.getActionType())); @@ -1866,4 +1868,69 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest .andExpect(jsonPath("total", equalTo(1))).andExpect(jsonPath("content[0].key", equalTo("knownKey1"))) .andExpect(jsonPath("content[0].value", equalTo("knownValue1"))); } + + @Test + @Description("A request for assigning multiple DS to a target results in a Bad Request when multiassignment in disabled.") + public void multiassignmentRequestNotAllowedIfDisabled() throws Exception { + final String targetId = testdataFactory.createTarget().getControllerId(); + final List dsIds = testdataFactory.createDistributionSets(2).stream().map(DistributionSet::getId) + .collect(Collectors.toList()); + + final JSONArray body = getAssignmentBody(dsIds); + + mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(body.toString()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + } + + @Test + @Description("Passing an array in assignment request is allowed if multiassignment is disabled and array size in 1.") + public void multiassignmentRequestAllowedIfDisabledButHasSizeOne() throws Exception { + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + + final JSONArray body = getAssignmentBody(Collections.singletonList(dsId)); + + mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(body.toString()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + } + + @Test + @Description("Identical assignments in a single request are removed when multiassignment in disabled.") + public void identicalAssignmentInRequestAreRemovedIfMultiassignmentsDisabled() throws Exception { + final String targetId = testdataFactory.createTarget().getControllerId(); + final Long dsId = testdataFactory.createDistributionSet().getId(); + + final JSONArray body = getAssignmentBody(Arrays.asList(dsId, dsId)); + + 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(1))); + } + + @Test + @Description("Assign multiple DSs to a target in one request with multiassignments enabled.") + public void multiAssignment() throws Exception { + final String targetId = testdataFactory.createTarget().getControllerId(); + final List dsIds = testdataFactory.createDistributionSets(2).stream().map(DistributionSet::getId) + .collect(Collectors.toList()); + + final JSONArray body = getAssignmentBody(dsIds); + + 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))); + } + + 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; + } } 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 77bff376c..5d484405d 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 @@ -80,6 +80,7 @@ public class ResponseExceptionHandler { ERROR_TO_HTTP_STATUS.put(SpServerError.SP_AUTO_ASSIGN_ACTION_TYPE_INVALID, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_AUTO_ASSIGN_DISTRIBUTION_SET_INVALID, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED, HttpStatus.FORBIDDEN); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_MULTIASSIGNMENT_NOT_ENABLED, HttpStatus.BAD_REQUEST); } private static HttpStatus getStatusOrDefault(final SpServerError error) { diff --git a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/targets-api-guide.adoc b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/targets-api-guide.adoc index 9ab095fb0..ecfbf4254 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/targets-api-guide.adoc +++ b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/targets-api-guide.adoc @@ -547,13 +547,13 @@ include::../errors/429.adoc[] |=== -== POST /rest/v1/targets/{targetId}/assignedDS +== POST /rest/v1/targets/{targetId}/assignedDS (assign single distribution set) === Implementation Notes Handles the POST request for assigning a distribution set to a specific target. Required Permission: READ_REPOSITORY and UPDATE_TARGET -=== Asssin distribution set to target +=== Assign distribution set to target ==== Curl @@ -601,6 +601,60 @@ include::../errors/429.adoc[] |=== +== POST /rest/v1/targets/{targetId}/assignedDS (assign multiple distribution sets) + +=== Implementation Notes + +Handles the POST request for assigning multiple distribution sets to a specific target (only allowed id 'multi assignments' is enabled). Required Permission: READ_REPOSITORY and UPDATE_TARGET + +=== Assign distribution sets to target + +==== Curl + +include::{snippets}/targets/post-assign-distribution-sets-to-target/curl-request.adoc[] + +==== Request path parameter + +include::{snippets}/targets/post-assign-distribution-sets-to-target/path-parameters.adoc[] + +==== Request query parameter + +include::{snippets}/targets/post-assign-distribution-sets-to-target/request-parameters.adoc[] + +==== Request fields + +include::{snippets}/targets/post-assign-distribution-sets-to-target/request-fields.adoc[] + +==== Request URL + +include::{snippets}/targets/post-assign-distribution-sets-to-target/http-request.adoc[] + +=== Response (Status 200) + +==== Response fields +include::{snippets}/targets/post-assign-distribution-sets-to-target/response-fields.adoc[] + +==== Response example + +include::{snippets}/targets/post-assign-distribution-sets-to-target/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400_multiassignment.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +include::../errors/404.adoc[] +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/409.adoc[] +include::../errors/415.adoc[] +include::../errors/429.adoc[] +|=== + + == GET /rest/v1/targets/{targetId}/attributes === Implementation Notes diff --git a/hawkbit-rest/hawkbit-rest-docs/src/main/errors/400_multiassignment.adoc b/hawkbit-rest/hawkbit-rest-docs/src/main/errors/400_multiassignment.adoc new file mode 100644 index 000000000..e502b8fbe --- /dev/null +++ b/hawkbit-rest/hawkbit-rest-docs/src/main/errors/400_multiassignment.adoc @@ -0,0 +1,3 @@ +| `400 Bad Request` +| Bad Request - e.g. invalid parameters or 'multi assignments' is disabled +| diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/RootControllerDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/RootControllerDocumentationTest.java index 892b83cac..b596e8621 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/RootControllerDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/RootControllerDocumentationTest.java @@ -21,7 +21,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.ByteArrayInputStream; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -73,7 +72,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio final DistributionSet set = testdataFactory.createDistributionSet("one"); final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); - deploymentManagement.assignDistributionSet(set.getId(), Arrays.asList(target.getTargetWithActionType())); + assignDistributionSet(set.getId(), target.getControllerId()); mockMvc.perform(get(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}", tenantAware.getCurrentTenant(), target.getControllerId()).accept(MediaTypes.HAL_JSON_VALUE)) @@ -101,8 +100,8 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio final DistributionSet setTwo = testdataFactory.createDistributionSet("two"); final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); - deploymentManagement.assignDistributionSet(set.getId(), Arrays.asList(target.getTargetWithActionType())); - deploymentManagement.assignDistributionSet(setTwo.getId(), Arrays.asList(target.getTargetWithActionType())); + assignDistributionSet(set.getId(), target.getControllerId()); + assignDistributionSet(setTwo.getId(), target.getControllerId()); mockMvc.perform(get(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}", tenantAware.getCurrentTenant(), target.getControllerId()).accept(MediaTypes.HAL_JSON_VALUE)) @@ -137,8 +136,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio }); final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); - final Long actionId = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(set.getId(), - Arrays.asList(target.getTargetWithActionType()))); + final Long actionId = getFirstAssignedActionId(assignDistributionSet(set.getId(), target.getControllerId())); final Action cancelAction = deploymentManagement.cancelAction(actionId); mockMvc.perform( @@ -169,8 +167,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio final DistributionSet set = testdataFactory.createDistributionSet("one"); final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); - final Long actionId = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(set.getId(), - Arrays.asList(target.getTargetWithActionType()))); + final Long actionId = getFirstAssignedActionId(assignDistributionSet(set.getId(), target.getControllerId())); final Action cancelAction = deploymentManagement.cancelAction(actionId); mockMvc.perform(post( @@ -387,8 +384,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio final DistributionSet set = testdataFactory.createDistributionSet("one"); final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); - final Long actionId = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(set.getId(), - Arrays.asList(target.getTargetWithActionType()))); + final Long actionId = getFirstAssignedActionId(assignDistributionSet(set.getId(), target.getControllerId())); mockMvc.perform(post(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/" + DdiRestConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}/feedback", tenantAware.getCurrentTenant(), @@ -434,7 +430,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio .create(new ArtifactUpload(new ByteArrayInputStream(random), module.getId(), "binaryFile", false, 0)); final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); - deploymentManagement.assignDistributionSet(set.getId(), Arrays.asList(target.getTargetWithActionType())); + assignDistributionSet(set.getId(), target.getControllerId()); mockMvc.perform( get(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/softwaremodules/{moduleId}/artifacts", 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 5a063c648..fa2960899 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 @@ -27,6 +27,7 @@ import org.eclipse.hawkbit.ddi.rest.resource.DdiApiConfiguration; import org.eclipse.hawkbit.mgmt.rest.resource.MgmtApiConfiguration; 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.DistributionSet; @@ -169,7 +170,8 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati private List assignWithoutMaintenanceWindow(final DistributionSet distributionSet, final Target savedTarget, final boolean timeforced) { final List actions = timeforced - ? assignDistributionSetTimeForced(distributionSet, savedTarget).getAssignedEntity() + ? assignDistributionSet(distributionSet.getId(), savedTarget.getControllerId(), ActionType.TIMEFORCED) + .getAssignedEntity() : assignDistributionSet(distributionSet, savedTarget).getAssignedEntity(); return actions.stream().map(Action::getTarget).collect(Collectors.toList()); } @@ -178,8 +180,8 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati final boolean timeforced, final String maintenanceWindowSchedule, final String maintenanceWindowDuration, final String maintenanceWindowTimeZone) { final List actions = timeforced - ? assignDistributionSetWithMaintenanceWindowTimeForced(distributionSet.getId(), - savedTarget.getControllerId(), maintenanceWindowSchedule, maintenanceWindowDuration, + ? assignDistributionSetWithMaintenanceWindow(distributionSet.getId(), savedTarget.getControllerId(), + ActionType.TIMEFORCED, maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone).getAssignedEntity() : assignDistributionSetWithMaintenanceWindow(distributionSet.getId(), savedTarget.getControllerId(), maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone) 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 b38a57d5a..15a7f3b8b 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 @@ -188,7 +188,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 FORCETIME_TYPE = "The type of the forcetime."; + 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."; public static final String TARGET_ASSIGNED_TOTAL = "The total number of targets that are part of this operation."; 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 e7247a50b..e793f395a 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 @@ -383,17 +383,20 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat parameterWithName("distributionSetId").description(ApiModelPropertiesGeneric.ITEM_ID)), requestParameters(parameterWithName("offline") .description(MgmtApiModelProperties.OFFLINE_UPDATE).optional()), - requestFields(requestFieldWithPath("[]forcetime").description(MgmtApiModelProperties.FORCETIME), - requestFieldWithPath("[]id").description(ApiModelPropertiesGeneric.ITEM_ID), - requestFieldWithPath("[]maintenanceWindow") - .description(MgmtApiModelProperties.MAINTENANCE_WINDOW).optional(), - requestFieldWithPath("[]maintenanceWindow.schedule") - .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_SCHEDULE).optional(), - requestFieldWithPath("[]maintenanceWindow.duration") - .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_DURATION).optional(), - requestFieldWithPath("[]maintenanceWindow.timezone") - .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE).optional(), - requestFieldWithPath("[]type").description(MgmtApiModelProperties.FORCETIME_TYPE) + requestFields( + requestFieldWithPath("[].id").description(ApiModelPropertiesGeneric.ITEM_ID), + optionalRequestFieldWithPath("[].forcetime") + .description(MgmtApiModelProperties.FORCETIME), + optionalRequestFieldWithPath("[].maintenanceWindow") + .description(MgmtApiModelProperties.MAINTENANCE_WINDOW), + optionalRequestFieldWithPath("[].maintenanceWindow.schedule") + .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_SCHEDULE), + optionalRequestFieldWithPath("[].maintenanceWindow.duration") + .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_DURATION), + optionalRequestFieldWithPath("[].maintenanceWindow.timezone") + .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE), + optionalRequestFieldWithPath("[].type") + .description(MgmtApiModelProperties.ASSIGNMENT_TYPE) .attributes(key("value").value("['soft', 'forced','timeforced', 'downloadonly']"))), responseFields( fieldWithPath("assigned").description(MgmtApiModelProperties.DS_NEW_ASSIGNED_TARGETS), 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 fc2ab7487..d9da9d456 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 @@ -48,6 +48,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort.Direction; import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import com.fasterxml.jackson.core.JsonProcessingException; @@ -395,8 +396,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio public void switchActionToForced() throws Exception { final Target target = testdataFactory.createTarget(targetId); final DistributionSet set = testdataFactory.createDistributionSet(); - final Long actionId = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(set.getId(), - ActionType.SOFT, 0, Collections.singletonList(target.getControllerId()))); + final Long actionId = getFirstAssignedActionId( + assignDistributionSet(set.getId(), target.getControllerId(), ActionType.SOFT)); assertThat(deploymentManagement.findAction(actionId).get().getActionType()).isEqualTo(ActionType.SOFT); final Map body = new HashMap<>(); @@ -485,7 +486,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)), getResponseFieldsDistributionSet(false))); } - + @Test @Description("Handles the POST request for assigning a distribution set to a specific target. Required Permission: READ_REPOSITORY and UPDATE_TARGET.") public void postAssignDistributionSetToTarget() throws Exception { @@ -511,30 +512,80 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)), requestParameters(parameterWithName("offline") .description(MgmtApiModelProperties.OFFLINE_UPDATE).optional()), - requestFields(requestFieldWithPath("forcetime").description(MgmtApiModelProperties.FORCETIME), + requestFields( requestFieldWithPath("id").description(ApiModelPropertiesGeneric.ITEM_ID), - requestFieldWithPath("maintenanceWindow") - .description(MgmtApiModelProperties.MAINTENANCE_WINDOW).optional(), - requestFieldWithPath("maintenanceWindow.schedule") - .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_SCHEDULE).optional(), - requestFieldWithPath("maintenanceWindow.duration") - .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_DURATION).optional(), - requestFieldWithPath("maintenanceWindow.timezone") - .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE).optional(), - requestFieldWithPath("type").description(MgmtApiModelProperties.FORCETIME_TYPE) + optionalRequestFieldWithPath("forcetime").description(MgmtApiModelProperties.FORCETIME), + optionalRequestFieldWithPath("maintenanceWindow") + .description(MgmtApiModelProperties.MAINTENANCE_WINDOW), + optionalRequestFieldWithPath("maintenanceWindow.schedule") + .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_SCHEDULE), + optionalRequestFieldWithPath("maintenanceWindow.duration") + .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_DURATION), + optionalRequestFieldWithPath("maintenanceWindow.timezone") + .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE), + optionalRequestFieldWithPath("type").description(MgmtApiModelProperties.ASSIGNMENT_TYPE) .attributes(key("value").value("['soft', 'forced','timeforced', 'downloadonly']"))), - responseFields( - fieldWithPath("assigned").description(MgmtApiModelProperties.DS_NEW_ASSIGNED_TARGETS), - fieldWithPath("alreadyAssigned").type(JsonFieldType.NUMBER) - .description(MgmtApiModelProperties.DS_ALREADY_ASSIGNED_TARGETS), - fieldWithPath("assignedActions").type(JsonFieldType.ARRAY) - .description(MgmtApiModelProperties.DS_NEW_ASSIGNED_ACTIONS), - fieldWithPath("assignedActions.[].id").type(JsonFieldType.NUMBER) - .description(MgmtApiModelProperties.ACTION_ID), - fieldWithPath("assignedActions.[]._links.self").type(JsonFieldType.OBJECT) - .description(MgmtApiModelProperties.LINK_TO_ACTION), - fieldWithPath("total").type(JsonFieldType.NUMBER) - .description(MgmtApiModelProperties.DS_TOTAL_ASSIGNED_TARGETS)))); + responseFields(getDsAssignmentResponseFieldDescriptors()))); + } + + @Test + @Description("Handles the POST request for assigning distribution sets to a specific target. Required Permission: READ_REPOSITORY and UPDATE_TARGET.") + public void postAssignDistributionSetsToTarget() throws Exception { + // create target and ds, and assign ds + final List sets = testdataFactory.createDistributionSets(2); + testdataFactory.createTarget(targetId); + + final long forceTime = System.currentTimeMillis(); + final JSONArray body = new JSONArray(); + body.put(new JSONObject().put("id", sets.get(1).getId()).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")); + + enableMultiAssignments(); + mockMvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + + MgmtRestConstants.TARGET_V1_ASSIGNED_DISTRIBUTION_SET, targetId).content(body.toString()) + .contentType(MediaType.APPLICATION_JSON_UTF8)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(this.document.document( + pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)), + requestParameters(parameterWithName("offline") + .description(MgmtApiModelProperties.OFFLINE_UPDATE).optional()), + requestFields( + requestFieldWithPath("[].id").description(ApiModelPropertiesGeneric.ITEM_ID), + optionalRequestFieldWithPath("[].forcetime") + .description(MgmtApiModelProperties.FORCETIME), + optionalRequestFieldWithPath("[].maintenanceWindow") + .description(MgmtApiModelProperties.MAINTENANCE_WINDOW), + optionalRequestFieldWithPath("[].maintenanceWindow.schedule") + .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_SCHEDULE), + optionalRequestFieldWithPath("[].maintenanceWindow.duration") + .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_DURATION), + optionalRequestFieldWithPath("[].maintenanceWindow.timezone") + .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE), + optionalRequestFieldWithPath("[].type") + .description(MgmtApiModelProperties.ASSIGNMENT_TYPE) + .attributes(key("[].value") + .value("['soft', 'forced','timeforced', 'downloadonly']"))), + responseFields(getDsAssignmentResponseFieldDescriptors()))); + } + + private static FieldDescriptor[] getDsAssignmentResponseFieldDescriptors() { + final FieldDescriptor[] descriptors = { + fieldWithPath("assigned").description(MgmtApiModelProperties.DS_NEW_ASSIGNED_TARGETS), + fieldWithPath("alreadyAssigned").type(JsonFieldType.NUMBER) + .description(MgmtApiModelProperties.DS_ALREADY_ASSIGNED_TARGETS), + fieldWithPath("assignedActions").type(JsonFieldType.ARRAY) + .description(MgmtApiModelProperties.DS_NEW_ASSIGNED_ACTIONS), + fieldWithPath("assignedActions.[].id").type(JsonFieldType.NUMBER) + .description(MgmtApiModelProperties.ACTION_ID), + fieldWithPath("assignedActions.[]._links.self").type(JsonFieldType.OBJECT) + .description(MgmtApiModelProperties.LINK_TO_ACTION), + fieldWithPath("total").type(JsonFieldType.NUMBER) + .description(MgmtApiModelProperties.DS_TOTAL_ASSIGNED_TARGETS) }; + return descriptors; } @Test diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java index 676e1b5f0..1f0b1943a 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java @@ -245,15 +245,15 @@ public class HawkbitSecurityProperties { private int maxTargetsPerRolloutGroup = 20000; /** - * Maximum number of targets per rollout group + * Maximum number of overall actions targets per target */ private int maxActionsPerTarget = 2000; /** - * Maximum number of targets for a manual distribution set assignment. - * Must be greater than 1000. + * Maximum number of actions resulting from a manual assignment of + * distribution sets and targets. Must be greater than 1000. */ - private int maxTargetsPerManualAssignment = 5000; + private int maxTargetDistributionSetAssignmentsPerManualAssignment = 5000; /** * Maximum number of targets for an automatic distribution set @@ -379,14 +379,15 @@ public class HawkbitSecurityProperties { this.maxActionsPerTarget = maxActionsPerTarget; } - public int getMaxTargetsPerManualAssignment() { - return maxTargetsPerManualAssignment; + public int getMaxTargetDistributionSetAssignmentsPerManualAssignment() { + return maxTargetDistributionSetAssignmentsPerManualAssignment; } - - public void setMaxTargetsPerManualAssignment(final int maxTargetsPerManualAssignment) { - this.maxTargetsPerManualAssignment = maxTargetsPerManualAssignment; + + public void setMaxTargetDistributionSetAssignmentsPerManualAssignment( + final int maxTargetDistributionSetAssignmentsPerManualAssignment) { + this.maxTargetDistributionSetAssignmentsPerManualAssignment = maxTargetDistributionSetAssignmentsPerManualAssignment; } - + public int getMaxTargetsPerAutoAssignment() { return maxTargetsPerAutoAssignment; } 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 925aec7e5..59f101f11 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 @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.ui.management; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; @@ -17,12 +18,14 @@ import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException; +import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.DeploymentRequest; +import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; import org.eclipse.hawkbit.repository.model.Target; -import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.ui.UiProperties; import org.eclipse.hawkbit.ui.common.confirmwindow.layout.ConfirmationTab; import org.eclipse.hawkbit.ui.common.entity.TargetIdName; @@ -80,6 +83,7 @@ public final class TargetAssignmentOperations { * the Vaadin Message Source for multi language * @param eventSource * the source object for sending potential events + * @throws MultiAssignmentIsNotEnabledException */ public static void saveAllAssignments(final List targets, final List distributionSets, final ManagementUIState managementUIState, @@ -101,31 +105,39 @@ public final class TargetAssignmentOperations { final String maintenanceTimeZone = maintenanceWindowLayout.getMaintenanceTimeZone(); final Set dsIds = distributionSets.stream().map(DistributionSet::getId).collect(Collectors.toSet()); - final List trgActionType = targets.stream() - .map(t -> maintenanceWindowLayout.isEnabled() - ? new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp, - maintenanceSchedule, maintenanceDuration, maintenanceTimeZone) - : new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp)) - .collect(Collectors.toList()); + final List deploymentRequests = new ArrayList<>(); + dsIds.forEach(dsId -> targets.forEach(t -> { + final DeploymentRequestBuilder request = DeploymentManagement.deploymentRequest(t.getControllerId(), dsId) + .setActionType(actionType).setForceTime(forcedTimeStamp); + if (maintenanceWindowLayout.isEnabled()) { + request.setMaintenance(maintenanceSchedule, maintenanceDuration, maintenanceTimeZone); + } + deploymentRequests.add(request.build()); + })); - final List results = deploymentManagement.assignDistributionSets(dsIds, - trgActionType); + try { + final List results = deploymentManagement + .assignDistributionSets(deploymentRequests); + // use the last one for the notification box + final DistributionSetAssignmentResult assignmentResult = results.get(results.size() - 1); + if (assignmentResult.getAssigned() > 0) { + notification + .displaySuccess(i18n.getMessage("message.target.assignment", assignmentResult.getAssigned())); + } + if (assignmentResult.getAlreadyAssigned() > 0) { + notification.displaySuccess( + i18n.getMessage("message.target.alreadyAssigned", assignmentResult.getAlreadyAssigned())); + } - // use the last one for the notification box - final DistributionSetAssignmentResult assignmentResult = results.get(results.size() - 1); - if (assignmentResult.getAssigned() > 0) { - notification.displaySuccess(i18n.getMessage("message.target.assignment", assignmentResult.getAssigned())); + final Set targetIds = targets.stream().map(Target::getId).collect(Collectors.toSet()); + refreshPinnedDetails(dsIds, targetIds, managementUIState, eventBus, eventSource); + + notification.displaySuccess(i18n.getMessage("message.target.ds.assign.success")); + eventBus.publish(eventSource, SaveActionWindowEvent.SAVED_ASSIGNMENTS); + } catch (final MultiAssignmentIsNotEnabledException e) { + notification.displayValidationError(i18n.getMessage("message.target.ds.multiassign.error")); + LOG.error("UI allowed multiassignment although it is not enabled: {}", e); } - if (assignmentResult.getAlreadyAssigned() > 0) { - notification.displaySuccess( - i18n.getMessage("message.target.alreadyAssigned", assignmentResult.getAlreadyAssigned())); - } - - final Set targetIds = targets.stream().map(Target::getId).collect(Collectors.toSet()); - refreshPinnedDetails(dsIds, targetIds, managementUIState, eventBus, eventSource); - - notification.displaySuccess(i18n.getMessage("message.target.ds.assign.success")); - eventBus.publish(eventSource, SaveActionWindowEvent.SAVED_ASSIGNMENTS); } private static void refreshPinnedDetails(final Set dsIds, final Set targetIds, @@ -275,4 +287,4 @@ public final class TargetAssignmentOperations { return SPUIComponentProvider.getHelpLink(i18n, maintenanceWindowHelpUrl); } -} \ No newline at end of file +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/BulkUploadHandler.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/BulkUploadHandler.java index 627f263d4..49a794f81 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/BulkUploadHandler.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/BulkUploadHandler.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.Executor; +import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; @@ -32,6 +33,7 @@ import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetTagManagement; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.ui.common.tagdetails.TagData; import org.eclipse.hawkbit.ui.components.HawkbitErrorNotificationMessage; import org.eclipse.hawkbit.ui.management.event.BulkUploadValidationMessageEvent; @@ -300,13 +302,17 @@ public class BulkUploadHandler extends CustomComponent final ActionType actionType = ActionType.FORCED; final long forcedTimeStamp = new Date().getTime(); final TargetBulkUpload targetBulkUpload = managementUIState.getTargetTableFilters().getBulkUpload(); - final List targetsList = targetBulkUpload.getTargetsCreated(); + final List targetIds = targetBulkUpload.getTargetsCreated(); final Long dsSelected = (Long) comboBox.getValue(); if (!distributionSetManagement.get(dsSelected).isPresent()) { return i18n.getMessage("message.bulk.upload.assignment.failed"); } - deploymentManagement.assignDistributionSet(targetBulkUpload.getDsNameAndVersion(), actionType, - forcedTimeStamp, targetsList); + final List deploymentRequests = targetIds.stream() + .map(targetId -> DeploymentManagement + .deploymentRequest(targetId, targetBulkUpload.getDsNameAndVersion()) + .setActionType(actionType).setForceTime(forcedTimeStamp).build()) + .collect(Collectors.toList()); + deploymentManagement.assignDistributionSets(deploymentRequests); return null; } diff --git a/hawkbit-ui/src/main/resources/messages.properties b/hawkbit-ui/src/main/resources/messages.properties index 613428475..6e02df9bc 100644 --- a/hawkbit-ui/src/main/resources/messages.properties +++ b/hawkbit-ui/src/main/resources/messages.properties @@ -516,6 +516,7 @@ message.dist.type.discard.success = All Distribution Types are discarded success message.dist.discard.success = All Distributions are discarded successfully ! message.assign.discard.success = All assignments are discarded successfully ! message.target.ds.assign.success = Assignment saved successfully ! +message.target.ds.multiassign.error = Cannot assign multiple distribution sets to a target! message.bulk.upload.assignment.failed = Distribution set assignment failed as distribution set no longer exists! message.bulk.upload.result.success = Successful: {0} message.bulk.upload.result.fail = Failed: {0}