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 e685ba590a..7863a1f00b 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 @@ -191,6 +191,7 @@ public enum SpServerError { */ SP_CONFIGURATION_VALUE_INVALID("hawkbit.server.error.configValueInvalid", "The given configuration value is invalid."), + /** * */ @@ -232,7 +233,14 @@ public enum SpServerError { * invalid. */ SP_AUTO_ASSIGN_DISTRIBUTION_SET_INVALID("hawkbit.server.error.repo.invalidAutoAssignDistributionSet", - "The given distribution set for auto-assignment is invalid: it is either incomplete (i.e. mandatory modules are missing) or soft deleted"); + "The given distribution set for auto-assignment is invalid: it is either incomplete (i.e. mandatory modules are missing) or soft deleted"), + + /** + * Error message informing the user that the requested tenant configuration + * change is not allowed. + */ + SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED("hawkbit.server.error.repo.tenantConfigurationValueChangeNotAllowed", + "The requested tenant configuration value modification is not allowed."); private final String key; private final String message; diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java index 623f64ea19..7bd88dc04b 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java @@ -17,6 +17,7 @@ import org.eclipse.hawkbit.dmf.amqp.api.AmqpSettings; import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; @@ -240,9 +241,11 @@ public Binding bindDeadLetterQueueToDeadLetterExchange() { @Bean public AmqpMessageHandlerService amqpMessageHandlerService(final RabbitTemplate rabbitTemplate, final AmqpMessageDispatcherService amqpMessageDispatcherService, - final ControllerManagement controllerManagement, final EntityFactory entityFactory) { + final ControllerManagement controllerManagement, final EntityFactory entityFactory, + final SystemSecurityContext systemSecurityContext, + final TenantConfigurationManagement tenantConfigurationManagement) { return new AmqpMessageHandlerService(rabbitTemplate, amqpMessageDispatcherService, controllerManagement, - entityFactory); + entityFactory, systemSecurityContext, tenantConfigurationManagement); } /** @@ -319,10 +322,10 @@ AmqpMessageDispatcherService amqpMessageDispatcherService(final RabbitTemplate r final AmqpMessageSenderService amqpSenderService, final ArtifactUrlHandler artifactUrlHandler, final SystemSecurityContext systemSecurityContext, final SystemManagement systemManagement, final TargetManagement targetManagement, final DistributionSetManagement distributionSetManagement, - final SoftwareModuleManagement softwareModuleManagement) { + final SoftwareModuleManagement softwareModuleManagement, final DeploymentManagement deploymentManagement) { return new AmqpMessageDispatcherService(rabbitTemplate, amqpSenderService, artifactUrlHandler, systemSecurityContext, systemManagement, targetManagement, serviceMatcher, distributionSetManagement, - softwareModuleManagement); + softwareModuleManagement, deploymentManagement); } private static Map getTTLMaxArgsAuthenticationQueue() { diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java index 8fb6f8bbbf..c5ffd53ee6 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java @@ -8,8 +8,11 @@ */ package org.eclipse.hawkbit.amqp; +import static org.eclipse.hawkbit.repository.RepositoryConstants.MAX_ACTION_COUNT; + import java.net.URI; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -28,12 +31,15 @@ import org.eclipse.hawkbit.dmf.json.model.DmfArtifactHash; import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest; import org.eclipse.hawkbit.dmf.json.model.DmfMetadata; +import org.eclipse.hawkbit.dmf.json.model.DmfMultiActionRequest; import org.eclipse.hawkbit.dmf.json.model.DmfSoftwareModule; +import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.event.remote.MultiActionEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent; @@ -41,6 +47,7 @@ import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.ActionProperties; 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.SoftwareModuleMetadata; import org.eclipse.hawkbit.repository.model.Target; @@ -79,6 +86,7 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { private final TargetManagement targetManagement; private final ServiceMatcher serviceMatcher; private final DistributionSetManagement distributionSetManagement; + private final DeploymentManagement deploymentManagement; private final SoftwareModuleManagement softwareModuleManagement; /** @@ -107,7 +115,7 @@ protected AmqpMessageDispatcherService(final RabbitTemplate rabbitTemplate, final SystemSecurityContext systemSecurityContext, final SystemManagement systemManagement, final TargetManagement targetManagement, final ServiceMatcher serviceMatcher, final DistributionSetManagement distributionSetManagement, - final SoftwareModuleManagement softwareModuleManagement) { + final SoftwareModuleManagement softwareModuleManagement, final DeploymentManagement deploymentManagement) { super(rabbitTemplate); this.artifactUrlHandler = artifactUrlHandler; this.amqpSenderService = amqpSenderService; @@ -117,6 +125,7 @@ protected AmqpMessageDispatcherService(final RabbitTemplate rabbitTemplate, this.serviceMatcher = serviceMatcher; this.distributionSetManagement = distributionSetManagement; this.softwareModuleManagement = softwareModuleManagement; + this.deploymentManagement = deploymentManagement; } /** @@ -134,30 +143,112 @@ protected void targetAssignDistributionSet(final TargetAssignDistributionSetEven LOG.debug("targetAssignDistributionSet retrieved. I will forward it to DMF broker."); - distributionSetManagement.get(assignedEvent.getDistributionSetId()).ifPresent(set -> { + distributionSetManagement.get(assignedEvent.getDistributionSetId()).ifPresent(ds -> { - final Map> modules = Maps - .newHashMapWithExpectedSize(set.getModules().size()); - set.getModules() - .forEach( - module -> modules.put(module, - softwareModuleManagement.findMetaDataBySoftwareModuleIdAndTargetVisible( - PageRequest.of(0, RepositoryConstants.MAX_META_DATA_COUNT), module.getId()) - .getContent())); + final Map> softwareModules = getSoftwareModulesWithMetadata( + ds); targetManagement.getByControllerID(assignedEvent.getActions().keySet()).forEach( target -> sendUpdateMessageToTarget(assignedEvent.getActions().get(target.getControllerId()), - target, modules)); + target, softwareModules)); + + }); + } + + /** + * Listener for Multi-Action events. + * + * @param e + * the Multi-Action event to be processed + */ + @EventListener(classes = MultiActionEvent.class) + protected void onMultiAction(final MultiActionEvent e) { + if (isNotFromSelf(e)) { + return; + } + LOG.debug("MultiActionEvent received for {}", e.getControllerIds()); + sendMultiActionRequestMessages(e.getTenant(), e.getControllerIds()); + } + + protected void sendMultiActionRequestMessages(final String tenant, final List controllerIds) { + + final Map>> softwareModuleMetadata = new HashMap<>(); + targetManagement.getByControllerID(controllerIds).stream() + .filter(target -> IpUtil.isAmqpUri(target.getAddress())).forEach(target -> { + + final List activeActions = deploymentManagement + .findActiveActionsByTarget(PageRequest.of(0, MAX_ACTION_COUNT), target.getControllerId()) + .getContent(); + + activeActions.forEach(action -> { + final DistributionSet distributionSet = action.getDistributionSet(); + softwareModuleMetadata.computeIfAbsent(distributionSet.getId(), + id -> getSoftwareModulesWithMetadata(distributionSet)); + }); + + if (!activeActions.isEmpty()) { + sendMultiActionRequestToTarget(tenant, target, activeActions, softwareModuleMetadata); + } + + }); + + } + + protected void sendMultiActionRequestToTarget(final String tenant, final Target target, final List actions, + final Map>> softwareModulesPerDistributionSet) { + + final URI targetAdress = target.getAddress(); + if (!IpUtil.isAmqpUri(targetAdress) || CollectionUtils.isEmpty(actions)) { + return; + } + final DmfMultiActionRequest multiActionRequest = new DmfMultiActionRequest(); + actions.forEach(action -> { + final DmfActionRequest actionRequest = createDmfActionRequest(target, action, + softwareModulesPerDistributionSet.get(action.getDistributionSet().getId())); + multiActionRequest.addElement(getEventTypeForAction(action), actionRequest); }); + + final Message message = getMessageConverter().toMessage(multiActionRequest, + createConnectorMessagePropertiesEvent(tenant, target.getControllerId(), EventTopic.MULTI_ACTION)); + amqpSenderService.sendMessage(message, targetAdress); + + } + + private DmfActionRequest createDmfActionRequest(final Target target, final Action action, + final Map> softwareModules) { + if (action.isCancelingOrCanceled()) { + return createPlainActionRequest(action); + } + return createDownloadAndUpdateRequest(target, action, softwareModules); + + } + + private static DmfActionRequest createPlainActionRequest(final Action action) { + final DmfActionRequest actionRequest = new DmfActionRequest(); + actionRequest.setActionId(action.getId()); + return actionRequest; + } + + private DmfDownloadAndUpdateRequest createDownloadAndUpdateRequest(final Target target, final Action action, + final Map> softwareModules) { + final DmfDownloadAndUpdateRequest request = new DmfDownloadAndUpdateRequest(); + request.setActionId(action.getId()); + request.setTargetSecurityToken(systemSecurityContext.runAsSystem(target::getSecurityToken)); + if (softwareModules != null) { + softwareModules.entrySet() + .forEach(entry -> request.addSoftwareModule(convertToAmqpSoftwareModule(target, entry))); + } + return request; } /** * Method to get the type of event depending on whether the action is a - * DOWNLOAD_ONLY action or if it has a valid maintenance window available - * or not based on defined maintenance schedule. In case of no maintenance - * schedule or if there is a valid window available, the topic {@link EventTopic#DOWNLOAD_AND_INSTALL} is - * returned else {@link EventTopic#DOWNLOAD} is returned. + * DOWNLOAD_ONLY action or if it has a valid maintenance window available or + * not based on defined maintenance schedule. In case of no maintenance + * schedule or if there is a valid window available, the topic + * {@link EventTopic#DOWNLOAD_AND_INSTALL} is returned else + * {@link EventTopic#DOWNLOAD} is returned. * * @param action * current action properties. @@ -165,8 +256,24 @@ protected void targetAssignDistributionSet(final TargetAssignDistributionSetEven * @return {@link EventTopic} to use for message. */ private static EventTopic getEventTypeForTarget(final ActionProperties action) { - return (Action.ActionType.DOWNLOAD_ONLY.equals(action.getActionType()) || - !action.isMaintenanceWindowAvailable()) ? EventTopic.DOWNLOAD : EventTopic.DOWNLOAD_AND_INSTALL; + return (Action.ActionType.DOWNLOAD_ONLY.equals(action.getActionType()) + || !action.isMaintenanceWindowAvailable()) ? EventTopic.DOWNLOAD : EventTopic.DOWNLOAD_AND_INSTALL; + } + + /** + * Determines the {@link EventTopic} for the given {@link Action}, depending + * on its action type. + * + * @param action + * to obtain the corresponding {@link EventTopic} for + * + * @return the {@link EventTopic} for this action + */ + private static EventTopic getEventTypeForAction(final Action action) { + if (action.isCancelingOrCanceled()) { + return EventTopic.CANCEL_DOWNLOAD; + } + return getEventTypeForTarget(new ActionProperties(action)); } /** @@ -174,7 +281,7 @@ private static EventTopic getEventTypeForTarget(final ActionProperties action) { * the Distribution set to a Target has been canceled. * * @param cancelEvent - * the object to be send. + * that is to be converted to a DMF message */ @EventListener(classes = CancelTargetAssignmentEvent.class) protected void targetCancelAssignmentToDistributionSet(final CancelTargetAssignmentEvent cancelEvent) { @@ -211,7 +318,7 @@ protected void targetTriggerUpdateAttributes(final TargetAttributesRequestedEven protected void sendUpdateMessageToTarget(final ActionProperties action, final Target target, final Map> modules) { - String tenant = action.getTenant(); + final String tenant = action.getTenant(); final URI targetAdress = target.getAddress(); if (!IpUtil.isAmqpUri(targetAdress)) { @@ -361,4 +468,17 @@ private DmfArtifact convertArtifact(final Target target, final Artifact localArt return artifact; } -} + private Map> getSoftwareModulesWithMetadata( + final DistributionSet distributionSet) { + final Map> moduleMetadata = Maps + .newHashMapWithExpectedSize(distributionSet.getModules().size()); + distributionSet.getModules() + .forEach( + module -> moduleMetadata.put(module, + softwareModuleManagement.findMetaDataBySoftwareModuleIdAndTargetVisible( + PageRequest.of(0, RepositoryConstants.MAX_META_DATA_COUNT), module.getId()) + .getContent())); + return moduleMetadata; + } + +} \ No newline at end of file diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java index feccb9d7ac..9247243ba3 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java @@ -8,11 +8,16 @@ */ package org.eclipse.hawkbit.amqp; +import static org.eclipse.hawkbit.repository.RepositoryConstants.MAX_ACTION_COUNT; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED; + +import java.io.Serializable; import java.net.URI; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -27,14 +32,17 @@ import org.eclipse.hawkbit.repository.ControllerManagement; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.RepositoryConstants; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.UpdateMode; import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionProperties; +import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.util.IpUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +50,7 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.data.domain.PageRequest; import org.springframework.messaging.handler.annotation.Header; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; @@ -51,8 +60,6 @@ import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.util.StringUtils; -import com.google.common.collect.Maps; - /** * * {@link AmqpMessageHandlerService} handles all incoming target interaction @@ -70,6 +77,10 @@ public class AmqpMessageHandlerService extends BaseAmqpService { private final EntityFactory entityFactory; + private final TenantConfigurationManagement tenantConfigurationManagement; + + private final SystemSecurityContext systemSecurityContext; + /** * Constructor. * @@ -84,11 +95,15 @@ public class AmqpMessageHandlerService extends BaseAmqpService { */ public AmqpMessageHandlerService(final RabbitTemplate rabbitTemplate, final AmqpMessageDispatcherService amqpMessageDispatcherService, - final ControllerManagement controllerManagement, final EntityFactory entityFactory) { + final ControllerManagement controllerManagement, final EntityFactory entityFactory, + final SystemSecurityContext systemSecurityContext, + final TenantConfigurationManagement tenantConfigurationManagement) { super(rabbitTemplate); this.amqpMessageDispatcherService = amqpMessageDispatcherService; this.controllerManagement = controllerManagement; this.entityFactory = entityFactory; + this.systemSecurityContext = systemSecurityContext; + this.tenantConfigurationManagement = tenantConfigurationManagement; } /** @@ -191,11 +206,31 @@ private void registerTarget(final Message message, final String virtualHost) { final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotexist(thingId, amqpUri); LOG.debug("Target {} reported online state.", thingId); - lookIfUpdateAvailable(target); + sendUpdateCommandToTarget(target); } - private void lookIfUpdateAvailable(final Target target) { + private void sendUpdateCommandToTarget(final Target target) { + if (isMultiAssignmentsEnabled()) { + sendCurrentActionsAsMultiActionToTarget(target); + } else { + sendOldestActionToTarget(target); + } + } + private void sendCurrentActionsAsMultiActionToTarget(final Target target) { + final List actions = controllerManagement + .findActiveActionsByTarget(PageRequest.of(0, MAX_ACTION_COUNT), target.getControllerId()).getContent(); + + final Set distributionSets = actions.stream().map(Action::getDistributionSet) + .collect(Collectors.toSet()); + final Map>> softwareModulesPerDistributionSet = distributionSets + .stream().collect(Collectors.toMap(DistributionSet::getId, this::getSoftwareModulesWithMetadata)); + + amqpMessageDispatcherService.sendMultiActionRequestToTarget(target.getTenant(), target, actions, + softwareModulesPerDistributionSet); + } + + private void sendOldestActionToTarget(final Target target) { final Optional actionOptional = controllerManagement .findOldestActiveActionByTarget(target.getControllerId()); @@ -207,20 +242,23 @@ private void lookIfUpdateAvailable(final Target target) { if (action.isCancelingOrCanceled()) { amqpMessageDispatcherService.sendCancelMessageToTarget(target.getTenant(), target.getControllerId(), action.getId(), target.getAddress()); - return; + } else { + amqpMessageDispatcherService.sendUpdateMessageToTarget(new ActionProperties(action), action.getTarget(), + getSoftwareModulesWithMetadata(action.getDistributionSet())); } + } - final Map> modules = Maps - .newHashMapWithExpectedSize(action.getDistributionSet().getModules().size()); + private Map> getSoftwareModulesWithMetadata( + final DistributionSet distributionSet) { + final List smIds = distributionSet.getModules().stream().map(SoftwareModule::getId) + .collect(Collectors.toList()); final Map> metadata = controllerManagement - .findTargetVisibleMetaDataBySoftwareModuleId(action.getDistributionSet().getModules().stream() - .map(SoftwareModule::getId).collect(Collectors.toList())); + .findTargetVisibleMetaDataBySoftwareModuleId(smIds); - action.getDistributionSet().getModules().forEach(module -> modules.put(module, metadata.get(module.getId()))); + return distributionSet.getModules().stream() + .collect(Collectors.toMap(sm -> sm, sm -> metadata.getOrDefault(sm.getId(), Collections.emptyList()))); - amqpMessageDispatcherService.sendUpdateMessageToTarget(new ActionProperties(action), action.getTarget(), - modules); } /** @@ -275,14 +313,19 @@ private void updateActionStatus(final Message message) { final ActionStatusCreate actionStatus = entityFactory.actionStatus().create(action.getId()).status(status) .messages(messages); - final Action addUpdateActionStatus = getUpdateActionStatus(status, actionStatus); + final Action updatedAction = Status.CANCELED.equals(status) + ? controllerManagement.addCancelActionStatus(actionStatus) + : controllerManagement.addUpdateActionStatus(actionStatus); - if (!addUpdateActionStatus.isActive() || (addUpdateActionStatus.hasMaintenanceSchedule() - && addUpdateActionStatus.isMaintenanceWindowAvailable())) { - lookIfUpdateAvailable(action.getTarget()); + if (shouldTargetProceed(updatedAction)) { + sendUpdateCommandToTarget(action.getTarget()); } } + private static boolean shouldTargetProceed(final Action action) { + return !action.isActive() || (action.hasMaintenanceSchedule() && action.isMaintenanceWindowAvailable()); + } + private static boolean isCorrelationIdNotEmpty(final Message message) { return StringUtils.hasLength(message.getMessageProperties().getCorrelationId()); } @@ -337,13 +380,6 @@ private Status handleCancelRejectedState(final Message message, final Action act return null; } - private Action getUpdateActionStatus(final Status status, final ActionStatusCreate actionStatus) { - if (Status.CANCELED.equals(status)) { - return controllerManagement.addCancelActionStatus(actionStatus); - } - return controllerManagement.addUpdateActionStatus(actionStatus); - } - // Exception squid:S3655 - logAndThrowMessageError throws exception, i.e. // get will not be called @SuppressWarnings("squid:S3655") @@ -373,4 +409,13 @@ private static UpdateMode getUpdateMode(final DmfAttributeUpdate update) { return null; } -} + private boolean isMultiAssignmentsEnabled() { + return getConfigValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class); + } + + private T getConfigValue(final String key, final Class valueType) { + return systemSecurityContext + .runAsSystem(() -> tenantConfigurationManagement.getConfigurationValue(key, valueType).getValue()); + } + +} \ No newline at end of file diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthenticationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthenticationTest.java index 5b36ceff94..386480ae56 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthenticationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthenticationTest.java @@ -16,14 +16,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import java.io.File; import java.net.URL; import java.util.Optional; import org.eclipse.hawkbit.api.HostnameResolver; -import org.eclipse.hawkbit.artifact.repository.ArtifactFilesystem; -import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact; -import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; import org.eclipse.hawkbit.cache.DownloadIdCache; import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; import org.eclipse.hawkbit.dmf.amqp.api.MessageType; @@ -109,7 +105,7 @@ public class AmqpControllerAuthenticationTest { private ControllerManagement controllerManagementMock; @Mock - private Target targteMock; + private Target targetMock; private static final TenantConfigurationValue CONFIG_VALUE_FALSE = TenantConfigurationValue . builder().value(Boolean.FALSE).build(); @@ -137,11 +133,11 @@ public void before() throws Exception { .thenReturn(CONFIG_VALUE_FALSE); final ControllerManagement controllerManagement = mock(ControllerManagement.class); - when(controllerManagement.getByControllerId(anyString())).thenReturn(Optional.of(targteMock)); - when(controllerManagement.get(any(Long.class))).thenReturn(Optional.of(targteMock)); + when(controllerManagement.getByControllerId(anyString())).thenReturn(Optional.of(targetMock)); + when(controllerManagement.get(any(Long.class))).thenReturn(Optional.of(targetMock)); - when(targteMock.getSecurityToken()).thenReturn(CONTROLLER_ID); - when(targteMock.getControllerId()).thenReturn(CONTROLLER_ID); + when(targetMock.getSecurityToken()).thenReturn(CONTROLLER_ID); + when(targetMock.getControllerId()).thenReturn(CONTROLLER_ID); final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(); final SystemSecurityContext systemSecurityContext = new SystemSecurityContext(tenantAware); @@ -162,11 +158,9 @@ public void before() throws Exception { when(artifactManagementMock.get(ARTIFACT_ID)).thenReturn(Optional.of(testArtifact)); when(artifactManagementMock.findFirstBySHA1(SHA1)).thenReturn(Optional.of(testArtifact)); - final AbstractDbArtifact artifact = new ArtifactFilesystem(new File("does not exist"), SHA1, - new DbArtifactHash(SHA1, "md5 test"), ARTIFACT_SIZE, null); - amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate, - mock(AmqpMessageDispatcherService.class), controllerManagementMock, new JpaEntityFactory()); + mock(AmqpMessageDispatcherService.class), controllerManagementMock, new JpaEntityFactory(), + systemSecurityContext, tenantConfigurationManagementMock); amqpAuthenticationMessageHandlerService = new AmqpAuthenticationMessageHandler(rabbitTemplate, authenticationManager, artifactManagementMock, cacheMock, hostnameResolverMock, diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java index b9db0f2d61..72381c4b14 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java @@ -13,7 +13,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyObject; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @@ -101,7 +100,7 @@ public void before() throws Exception { senderService = Mockito.mock(DefaultAmqpMessageSenderService.class); final ArtifactUrlHandler artifactUrlHandlerMock = Mockito.mock(ArtifactUrlHandler.class); - when(artifactUrlHandlerMock.getUrls(anyObject(), anyObject())) + when(artifactUrlHandlerMock.getUrls(any(), any())) .thenReturn(Arrays.asList(new ArtifactUrl("http", "download", "http://mockurl"))); systemManagement = Mockito.mock(SystemManagement.class); @@ -113,7 +112,7 @@ public void before() throws Exception { amqpMessageDispatcherService = new AmqpMessageDispatcherService(rabbitTemplate, senderService, artifactUrlHandlerMock, systemSecurityContext, systemManagement, targetManagement, serviceMatcher, - distributionSetManagement, softwareModuleManagement); + distributionSetManagement, softwareModuleManagement, deploymentManagement); } @@ -125,7 +124,7 @@ private Message getCaptureAdressEvent(final TargetAssignDistributionSetEvent tar } private Action createAction(final DistributionSet testDs) { - return deploymentManagement.findAction(assignDistributionSet(testDs, testTarget).getActions().get(0)).get(); + return deploymentManagement.findAction(assignDistributionSet(testDs, testTarget).getActionIds().get(0)).get(); } @Test diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java index 236fa43d84..0b6469efdb 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.amqp; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -39,6 +40,7 @@ import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.ControllerManagement; import org.eclipse.hawkbit.repository.EntityFactory; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.UpdateMode; import org.eclipse.hawkbit.repository.builder.ActionStatusBuilder; import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; @@ -49,9 +51,12 @@ import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TenantConfigurationValue; import org.eclipse.hawkbit.security.DmfTenantSecurityToken; import org.eclipse.hawkbit.security.DmfTenantSecurityToken.FileResource; +import org.eclipse.hawkbit.security.SecurityContextTenantAware; import org.eclipse.hawkbit.security.SecurityTokenGenerator; +import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; import org.junit.Before; import org.junit.Test; @@ -100,6 +105,9 @@ public class AmqpMessageHandlerServiceTest { @Mock private ArtifactManagement artifactManagementMock; + @Mock + private TenantConfigurationManagement tenantConfigurationManagement; + @Mock private AmqpControllerAuthentication authenticationManagerMock; @@ -128,16 +136,21 @@ public class AmqpMessageHandlerServiceTest { private ArgumentCaptor modeCaptor; @Before + @SuppressWarnings({ "rawtypes", "unchecked" }) public void before() throws Exception { messageConverter = new Jackson2JsonMessageConverter(); when(rabbitTemplate.getMessageConverter()).thenReturn(messageConverter); when(artifactManagementMock.findFirstBySHA1(SHA1)).thenReturn(Optional.empty()); + final TenantConfigurationValue multiAssignmentConfig = TenantConfigurationValue.builder().value(Boolean.FALSE) + .global(Boolean.FALSE).build(); + when(tenantConfigurationManagement.getConfigurationValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class)) + .thenReturn(multiAssignmentConfig); - amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate, amqpMessageDispatcherServiceMock, - controllerManagementMock, entityFactoryMock); + final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(); + final SystemSecurityContext systemSecurityContext = new SystemSecurityContext(tenantAware); amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate, amqpMessageDispatcherServiceMock, - controllerManagementMock, entityFactoryMock); + controllerManagementMock, entityFactoryMock, systemSecurityContext, tenantConfigurationManagement); amqpAuthenticationMessageHandlerService = new AmqpAuthenticationMessageHandler(rabbitTemplate, authenticationManagerMock, artifactManagementMock, downloadIdCache, hostnameResolverMock, controllerManagementMock, tenantAwareMock); @@ -465,8 +478,8 @@ public void lookupNextUpdateActionAfterFinished() throws IllegalAccessException final ArgumentCaptor actionPropertiesCaptor = ArgumentCaptor.forClass(ActionProperties.class); final ArgumentCaptor targetCaptor = ArgumentCaptor.forClass(Target.class); - verify(amqpMessageDispatcherServiceMock, times(1)) - .sendUpdateMessageToTarget(actionPropertiesCaptor.capture(), targetCaptor.capture(), any(Map.class)); + verify(amqpMessageDispatcherServiceMock, times(1)).sendUpdateMessageToTarget(actionPropertiesCaptor.capture(), + targetCaptor.capture(), any(Map.class)); final ActionProperties actionProperties = actionPropertiesCaptor.getValue(); assertThat(actionProperties).isNotNull(); assertThat(actionProperties.getTenant()).as("event has tenant").isEqualTo("DEFAULT"); diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java index f5b9c0c429..57fc5ac99d 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java @@ -11,6 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -69,8 +70,8 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt protected static final String TENANT_EXIST = "DEFAULT"; protected static final String CREATED_BY = "CONTROLLER_PLUG_AND_PLAY"; + protected ReplyToListener replyToListener; private DeadletterListener deadletterListener; - private ReplyToListener replyToListener; private DistributionSet distributionSet; @Autowired @@ -86,11 +87,12 @@ public void initListener() { Mockito.reset(deadletterListener); replyToListener = harness.getSpy(ReplyToListener.LISTENER_ID); assertThat(replyToListener).isNotNull(); + replyToListener.purge(); Mockito.reset(replyToListener); getDmfClient().setExchange(AmqpSettings.DMF_EXCHANGE); } - protected T waitUntilIsPresent(final Callable> callable) { + private T waitUntilIsPresent(final Callable> callable) { createConditionFactory().until(() -> { return securityRule.runAsPrivileged(() -> callable.call().isPresent()); }); @@ -102,10 +104,12 @@ protected T waitUntilIsPresent(final Callable> callable) { } } - protected void verifyDeadLetterMessages(final int expectedMessages) { + protected void waitUntilEventMessagesAreDispatchedToTarget(final EventTopic... eventTopics) { createConditionFactory().untilAsserted(() -> { - Mockito.verify(getDeadletterListener(), Mockito.times(expectedMessages)).handleMessage(Mockito.any()); + assertThat(replyToListener.getLatestEventMessageTopics()) + .containsExactlyInAnyOrderElementsOf(Arrays.asList(eventTopics)); }); + replyToListener.resetLatestEventMessageTopics(); } protected DeadletterListener getDeadletterListener() { @@ -158,7 +162,7 @@ protected void assertRequestAttributesUpdateMessage(final String target) { } protected void assertRequestAttributesUpdateMessageAbsent() { - assertThat(replyToListener.getEventTopicMessages()).doesNotContainKey(EventTopic.REQUEST_ATTRIBUTES_UPDATE); + assertThat(replyToListener.getEventMessages()).doesNotContainKey(EventTopic.REQUEST_ATTRIBUTES_UPDATE); } protected void assertPingReplyMessage(final String correlationId) { @@ -196,15 +200,17 @@ private void assertAssignmentMessage(final Set dsModules, final assertThat(updatedTarget.getSecurityToken()).isEqualTo(downloadAndUpdateRequest.getTargetSecurityToken()); } - protected void assertDownloadAndInstallMessage(final Set dsModules, final String controllerId) { - assertAssignmentMessage(dsModules, controllerId, EventTopic.DOWNLOAD_AND_INSTALL); + protected void assertDownloadAndInstallMessage(final Set softwareModules, + final String controllerId) { + assertAssignmentMessage(softwareModules, controllerId, EventTopic.DOWNLOAD_AND_INSTALL); + } protected void assertDownloadMessage(final Set dsModules, final String controllerId) { assertAssignmentMessage(dsModules, controllerId, EventTopic.DOWNLOAD); } - protected void createAndSendTarget(final String target, final String tenant) { + protected void createAndSendThingCreated(final String target, final String tenant) { final Message message = createTargetMessage(target, tenant); getDmfClient().send(message); } @@ -229,7 +235,7 @@ protected Long cancelAction(final Long actionId, final String controllerId) { protected Long registerTargetAndCancelActionId(final String controllerId) { final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId); - return cancelAction(assignmentResult.getActions().get(0), controllerId); + return cancelAction(assignmentResult.getActionIds().get(0), controllerId); } protected void assertAllTargetsCount(final long expectedTargetsCount) { @@ -238,7 +244,8 @@ protected void assertAllTargetsCount(final long expectedTargetsCount) { protected Message assertReplyMessageHeader(final EventTopic eventTopic, final String controllerId) { verifyReplyToListener(); - final Message replyMessage = replyToListener.getEventTopicMessages().get(eventTopic); + + final Message replyMessage = replyToListener.getLatestEventMessage(eventTopic); assertAllTargetsCount(1); final Map headers = replyMessage.getMessageProperties().getHeaders(); assertThat(headers.get(MessageHeaderKey.TOPIC)).isEqualTo(eventTopic.toString()); @@ -264,7 +271,7 @@ protected void registerAndAssertTargetWithExistingTenant(final String target, protected void registerAndAssertTargetWithExistingTenant(final String target, final int existingTargetsAfterCreation, final TargetUpdateStatus expectedTargetStatus, final String createdBy) { - createAndSendTarget(target, TENANT_EXIST); + createAndSendThingCreated(target, TENANT_EXIST); final Target registerdTarget = waitUntilIsPresent(() -> targetManagement.getByControllerID(target)); assertAllTargetsCount(existingTargetsAfterCreation); assertTarget(registerdTarget, expectedTargetStatus, createdBy); @@ -273,7 +280,7 @@ protected void registerAndAssertTargetWithExistingTenant(final String target, protected void registerSameTargetAndAssertBasedOnVersion(final String controllerId, final int existingTargetsAfterCreation, final TargetUpdateStatus expectedTargetStatus) { final int version = controllerManagement.getByControllerId(controllerId).get().getOptLockRevision(); - createAndSendTarget(controllerId, TENANT_EXIST); + createAndSendThingCreated(controllerId, TENANT_EXIST); final Target registeredTarget = waitUntilIsPresent(() -> findTargetBasedOnNewVersion(controllerId, version)); assertAllTargetsCount(existingTargetsAfterCreation); assertThat(registeredTarget.getUpdateStatus()).isEqualTo(expectedTargetStatus); @@ -315,7 +322,7 @@ protected Message createPingMessage(final String correlationId, final String ten return createMessage(null, messageProperties); } - protected Message createActionStatusUpdateMessage(final String target, final String tenant, final long actionId, + protected void createAndSendActionStatusUpdateMessage(final String target, final String tenant, final long actionId, final DmfActionStatus status) { final MessageProperties messageProperties = createMessagePropertiesWithTenant(tenant); messageProperties.getHeaders().put(MessageHeaderKey.THING_ID, target); @@ -323,7 +330,7 @@ protected Message createActionStatusUpdateMessage(final String target, final Str messageProperties.getHeaders().put(MessageHeaderKey.TOPIC, EventTopic.UPDATE_ACTION_STATUS.toString()); final DmfActionUpdateStatus dmfActionUpdateStatus = new DmfActionUpdateStatus(actionId, status); - return createMessage(dmfActionUpdateStatus, messageProperties); + getDmfClient().send(createMessage(dmfActionUpdateStatus, messageProperties)); } protected MessageProperties createMessagePropertiesWithTenant(final String tenant) { 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 9f177cf052..c2c626c647 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 @@ -8,13 +8,30 @@ */ package org.eclipse.hawkbit.integration; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.dmf.amqp.api.EventTopic.DOWNLOAD; +import static org.eclipse.hawkbit.dmf.amqp.api.MessageType.EVENT; +import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY; + +import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; +import java.util.stream.Collectors; import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; +import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus; +import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest; +import org.eclipse.hawkbit.dmf.json.model.DmfMultiActionRequest; +import org.eclipse.hawkbit.dmf.json.model.DmfMultiActionRequest.DmfMultiActionElement; +import org.eclipse.hawkbit.dmf.json.model.DmfSoftwareModule; +import org.eclipse.hawkbit.repository.event.remote.MultiActionEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent; @@ -23,6 +40,10 @@ import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.CancelTargetAssignmentEvent; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.RolloutCreatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.RolloutGroupCreatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.RolloutGroupUpdatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.RolloutUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; @@ -30,10 +51,13 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; +import org.eclipse.hawkbit.repository.model.Rollout; +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.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; @@ -42,11 +66,6 @@ import io.qameta.allure.Feature; import io.qameta.allure.Story; -import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.hawkbit.dmf.amqp.api.EventTopic.DOWNLOAD; -import static org.eclipse.hawkbit.dmf.amqp.api.MessageType.EVENT; -import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY; - @Feature("Component Tests - Device Management Federation API") @Story("Amqp Message Dispatcher Service") public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpServiceIntegrationTest { @@ -131,12 +150,220 @@ public void assignDistributionSetMultipleTimes() { final DistributionSet distributionSet2 = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); registerTargetAndAssignDistributionSet(distributionSet2.getId(), TargetUpdateStatus.PENDING, getDistributionSet().getModules(), controllerId); - assertCancelActionMessage(assignmentResult.getActions().get(0), controllerId); + assertCancelActionMessage(assignmentResult.getActionIds().get(0), controllerId); - createAndSendTarget(controllerId, TENANT_EXIST); + createAndSendThingCreated(controllerId, TENANT_EXIST); waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.PENDING); - assertCancelActionMessage(assignmentResult.getActions().get(0), controllerId); + assertCancelActionMessage(assignmentResult.getActionIds().get(0), controllerId); + + } + + @Test + @Description("If multi assignment is enabled multi-action messages are sent.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = MultiActionEvent.class, count = 2), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 0), + @Expect(type = CancelTargetAssignmentEvent.class, count = 0), + @Expect(type = ActionCreatedEvent.class, count = 2), @Expect(type = ActionUpdatedEvent.class, count = 0), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6), + @Expect(type = DistributionSetCreatedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 2), @Expect(type = TargetPollEvent.class, count = 1) }) + public void assignMultipleDsInMultiAssignMode() { + enableMultiAssignments(); + final String controllerId = TARGET_PREFIX + "assignMultipleDsInMultiAssignMode"; + registerAndAssertTargetWithExistingTenant(controllerId); + + final Long actionId1 = assignNewDsToTarget(controllerId); + final Entry action1Install = new SimpleEntry<>(actionId1, EventTopic.DOWNLOAD_AND_INSTALL); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); + assertLatestMultiActionMessage(controllerId, Arrays.asList(action1Install)); + + final Long actionId2 = assignNewDsToTarget(controllerId); + final Entry action2Install = new SimpleEntry<>(actionId2, EventTopic.DOWNLOAD_AND_INSTALL); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); + assertLatestMultiActionMessage(controllerId, Arrays.asList(action1Install, action2Install)); + } + + @Test + @Description("Handle cancelation process of an action in multi assignment mode.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = MultiActionEvent.class, count = 3), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 0), + @Expect(type = CancelTargetAssignmentEvent.class, count = 0), + @Expect(type = ActionCreatedEvent.class, count = 2), @Expect(type = ActionUpdatedEvent.class, count = 2), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6), + @Expect(type = DistributionSetCreatedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 2), @Expect(type = TargetPollEvent.class, count = 1) }) + public void cancelActionInMultiAssignMode() { + enableMultiAssignments(); + final String controllerId = TARGET_PREFIX + "cancelActionInMultiAssignMode"; + registerAndAssertTargetWithExistingTenant(controllerId); + + final long actionId1 = assignNewDsToTarget(controllerId); + final long actionId2 = assignNewDsToTarget(controllerId); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION, EventTopic.MULTI_ACTION); + deploymentManagement.cancelAction(actionId1); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); + + final Entry action1Cancel = new SimpleEntry<>(actionId1, EventTopic.CANCEL_DOWNLOAD); + final Entry action2Install = new SimpleEntry<>(actionId2, EventTopic.DOWNLOAD_AND_INSTALL); + + assertLatestMultiActionMessage(controllerId, Arrays.asList(action1Cancel, action2Install)); + updateActionViaDmfClient(controllerId, actionId1, DmfActionStatus.CANCELED); + + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); + assertLatestMultiActionMessage(controllerId, Arrays.asList(action2Install)); + } + + @Test + @Description("Handle finishing an action in multi assignment mode.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = MultiActionEvent.class, count = 2), + @Expect(type = TargetAttributesRequestedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 0), + @Expect(type = CancelTargetAssignmentEvent.class, count = 0), + @Expect(type = ActionCreatedEvent.class, count = 2), @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6), + @Expect(type = DistributionSetCreatedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 3), @Expect(type = TargetPollEvent.class, count = 1) }) + public void finishActionInMultiAssignMode() { + enableMultiAssignments(); + final String controllerId = TARGET_PREFIX + "finishActionInMultiAssignMode"; + registerAndAssertTargetWithExistingTenant(controllerId); + + final long actionId1 = assignNewDsToTarget(controllerId); + final long actionId2 = assignNewDsToTarget(controllerId); + final Entry action2Install = new SimpleEntry<>(actionId2, EventTopic.DOWNLOAD_AND_INSTALL); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION, EventTopic.MULTI_ACTION); + updateActionViaDmfClient(controllerId, actionId1, DmfActionStatus.FINISHED); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.REQUEST_ATTRIBUTES_UPDATE, EventTopic.MULTI_ACTION); + assertRequestAttributesUpdateMessage(controllerId); + assertLatestMultiActionMessage(controllerId, Arrays.asList(action2Install)); + } + + @Test + @Description("If multi assignment is enabled assigning a DS multiple times creates a new action every time.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = MultiActionEvent.class, count = 2), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 0), + @Expect(type = CancelTargetAssignmentEvent.class, count = 0), + @Expect(type = ActionCreatedEvent.class, count = 2), @Expect(type = ActionUpdatedEvent.class, count = 0), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), @Expect(type = TargetPollEvent.class, count = 1) }) + public void assignDsMultipleTimesInMultiAssignMode() { + enableMultiAssignments(); + final String controllerId = TARGET_PREFIX + "assignDsMultipleTimesInMultiAssignMode"; + registerAndAssertTargetWithExistingTenant(controllerId); + final DistributionSet ds = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + + final Long actionId1 = assignDistributionSet(ds.getId(), controllerId).getActionIds().get(0); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); + final Long actionId2 = assignDistributionSet(ds.getId(), controllerId).getActionIds().get(0); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); + + final Entry action1Install = new SimpleEntry<>(actionId1, EventTopic.DOWNLOAD_AND_INSTALL); + final Entry action2Install = new SimpleEntry<>(actionId2, EventTopic.DOWNLOAD_AND_INSTALL); + assertLatestMultiActionMessage(controllerId, Arrays.asList(action1Install, action2Install)); + } + + private void updateActionViaDmfClient(final String controllerId, final long actionId, + final DmfActionStatus status) { + createAndSendActionStatusUpdateMessage(controllerId, TENANT_EXIST, actionId, status); + } + + private Long assignNewDsToTarget(final String controllerId) { + final DistributionSet ds = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final Long actionId = assignDistributionSet(ds.getId(), controllerId).getActionIds().get(0); + waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.PENDING); + return actionId; + } + + @Test + @Description("If multi assignment is enabled multiple rollouts with the same DS lead to multiple actions.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = MultiActionEvent.class, count = 2), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 0), + @Expect(type = CancelTargetAssignmentEvent.class, count = 0), + @Expect(type = ActionCreatedEvent.class, count = 2), @Expect(type = ActionUpdatedEvent.class, count = 2), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1), + @Expect(type = RolloutCreatedEvent.class, count = 2), @Expect(type = RolloutUpdatedEvent.class, count = 6), + @Expect(type = RolloutGroupCreatedEvent.class, count = 2), + @Expect(type = RolloutGroupUpdatedEvent.class, count = 4) }) + public void startRolloutsWithSameDsInMultiAssignMode() { + enableMultiAssignments(); + final String controllerId = TARGET_PREFIX + "startRolloutsWithSameDsInMultiAssignMode"; + + registerAndAssertTargetWithExistingTenant(controllerId); + final DistributionSet ds = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final Set smIds = getSoftwareModuleIds(ds); + final String filterQuery = "controllerId==" + controllerId; + + createAndStartRollout(ds, filterQuery); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); + assertLatestMultiActionMessageContainsInstallMessages(controllerId, Arrays.asList(smIds)); + + createAndStartRollout(ds, filterQuery); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); + assertLatestMultiActionMessageContainsInstallMessages(controllerId, Arrays.asList(smIds, smIds)); + } + + @Test + @Description("If multi assignment is enabled finishing one rollout does not affect other rollouts of the target.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = MultiActionEvent.class, count = 3), @Expect(type = ActionCreatedEvent.class, count = 3), + @Expect(type = ActionUpdatedEvent.class, count = 5), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6), + @Expect(type = DistributionSetCreatedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 5), @Expect(type = TargetPollEvent.class, count = 1), + @Expect(type = TargetAttributesRequestedEvent.class, count = 2), + @Expect(type = RolloutCreatedEvent.class, count = 3), @Expect(type = RolloutUpdatedEvent.class, count = 9), + @Expect(type = RolloutGroupCreatedEvent.class, count = 3), + @Expect(type = RolloutGroupUpdatedEvent.class, count = 6) }) + public void startMultipleRolloutsAndFinishInMultiAssignMode() { + enableMultiAssignments(); + final String controllerId = TARGET_PREFIX + "startMultipleRolloutsAndFinishInMultiAssignMode"; + + registerAndAssertTargetWithExistingTenant(controllerId); + final String filterQuery = "controllerId==" + controllerId; + final DistributionSet ds1 = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final Set smIds1 = getSoftwareModuleIds(ds1); + final DistributionSet ds2 = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final Set smIds2 = getSoftwareModuleIds(ds2); + + createAndStartRollout(ds1, filterQuery); + createAndStartRollout(ds2, filterQuery); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION, EventTopic.MULTI_ACTION); + createAndStartRollout(ds1, filterQuery); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.MULTI_ACTION); + assertLatestMultiActionMessageContainsInstallMessages(controllerId, Arrays.asList(smIds1, smIds2, smIds1)); + + final List installActions = getLatestMultiActionMessageActions(controllerId).stream() + .filter(entry -> entry.getValue().equals(EventTopic.DOWNLOAD_AND_INSTALL)).map(Entry::getKey) + .collect(Collectors.toList()); + + updateActionViaDmfClient(controllerId, installActions.get(0), DmfActionStatus.FINISHED); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.REQUEST_ATTRIBUTES_UPDATE, EventTopic.MULTI_ACTION); + assertLatestMultiActionMessageContainsInstallMessages(controllerId, Arrays.asList(smIds2, smIds1)); + + updateActionViaDmfClient(controllerId, installActions.get(1), DmfActionStatus.FINISHED); + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.REQUEST_ATTRIBUTES_UPDATE, EventTopic.MULTI_ACTION); + assertLatestMultiActionMessageContainsInstallMessages(controllerId, Arrays.asList(smIds1)); + } + + private Set getSoftwareModuleIds(final DistributionSet ds) { + return ds.getModules().stream().map(SoftwareModule::getId).collect(Collectors.toSet()); + } + + private Rollout createAndStartRollout(final DistributionSet ds, final String filterQuery) { + final Rollout rollout = testdataFactory.createRolloutByVariables(UUID.randomUUID().toString(), "", 1, + filterQuery, ds, "50", "5"); + rolloutManagement.start(rollout.getId()); + rolloutManagement.handleRollouts(); + return rollout; } @Test @@ -154,7 +381,7 @@ public void sendCancelStatus() { final Long actionId = registerTargetAndCancelActionId(controllerId); - createAndSendTarget(controllerId, TENANT_EXIST); + createAndSendThingCreated(controllerId, TENANT_EXIST); waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.PENDING); assertCancelActionMessage(actionId, controllerId); } @@ -171,39 +398,28 @@ public void sendDeleteMessage() { assertDeleteMessage(controllerId); } - @Test @Description("Verify that attribute update is requested after device successfully closed software update.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 2), @Expect(type = ActionUpdatedEvent.class, count = 2), @Expect(type = ActionCreatedEvent.class, count = 2), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), - @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6), + @Expect(type = DistributionSetCreatedEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 4), @Expect(type = TargetAttributesRequestedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) }) public void attributeRequestAfterSuccessfulUpdate() { final String controllerId = TARGET_PREFIX + "attributeUpdateRequest"; registerAndAssertTargetWithExistingTenant(controllerId); - final Target target = controllerManagement.getByControllerId(controllerId).get(); - final DistributionSet distributionSet = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); - final long actionId1 = assignDistributionSet(distributionSet, target).getActions().get(0); - waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.PENDING); - final Message messageError = createActionStatusUpdateMessage(controllerId, TENANT_EXIST, actionId1, - DmfActionStatus.ERROR); - getDmfClient().send(messageError); + final long actionId1 = assignNewDsToTarget(controllerId); + updateActionViaDmfClient(controllerId, actionId1, DmfActionStatus.ERROR); waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.ERROR); - assertRequestAttributesUpdateMessageAbsent(); - final long actionId2 = assignDistributionSet(distributionSet, target).getActions().get(0); - waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.PENDING); - final Message messageFin = createActionStatusUpdateMessage(controllerId, TENANT_EXIST, actionId2, - DmfActionStatus.FINISHED); - getDmfClient().send(messageFin); + final long actionId2 = assignNewDsToTarget(controllerId); + updateActionViaDmfClient(controllerId, actionId2, DmfActionStatus.FINISHED); waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.IN_SYNC); - assertRequestAttributesUpdateMessage(controllerId); } @@ -220,7 +436,6 @@ public void downloadOnlyAssignmentSendsDownloadMessageTopic() { final String controllerId = TARGET_PREFIX + "registerTargets_1"; final DistributionSet distributionSet = createTargetAndDistributionSetAndAssign(controllerId, DOWNLOAD_ONLY); - // verify final Message message = assertReplyMessageHeader(EventTopic.DOWNLOAD, controllerId); Mockito.verifyZeroInteractions(getDeadletterListener()); @@ -249,4 +464,54 @@ private void waitUntilTargetHasStatus(final String controllerId, final TargetUpd private void waitUntil(final Callable callable) { 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); + assertThat(multiactionMessage.getMessageProperties().getHeaders().get(MessageHeaderKey.THING_ID)) + .isEqualTo(controllerId); + final DmfMultiActionRequest multiActionRequest = (DmfMultiActionRequest) getDmfClient().getMessageConverter() + .fromMessage(multiactionMessage); + + final List> smIdsOfActionsFound = getDownloadAndUpdateRequests(multiActionRequest).stream() + .map(AmqpMessageDispatcherServiceIntegrationTest::getSmIds).collect(Collectors.toList()); + assertThat(smIdsOfActionsFound).containsExactlyInAnyOrderElementsOf(smIdsOfActionsExpected); + } + + private void assertLatestMultiActionMessage(final String controllerId, + final List> actionsExpected) { + final List> actionsFromMessage = getLatestMultiActionMessageActions(controllerId); + assertThat(actionsFromMessage).containsExactlyInAnyOrderElementsOf(actionsExpected); + } + + private List> getLatestMultiActionMessageActions(final String expectedControllerId) { + final Message multiactionMessage = replyToListener.getLatestEventMessage(EventTopic.MULTI_ACTION); + assertThat(multiactionMessage.getMessageProperties().getHeaders().get(MessageHeaderKey.THING_ID)) + .isEqualTo(expectedControllerId); + final List multiActionRequest = ((DmfMultiActionRequest) getDmfClient() + .getMessageConverter().fromMessage(multiactionMessage)).getElements(); + return multiActionRequest.stream() + .map(request -> new SimpleEntry<>(request.getAction().getActionId(), request.getTopic())) + .collect(Collectors.toList()); + } + + private static Set getSmIds(final DmfDownloadAndUpdateRequest request) { + return request.getSoftwareModules().stream().map(DmfSoftwareModule::getModuleId).collect(Collectors.toSet()); + } + + private static List getDownloadAndUpdateRequests(final DmfMultiActionRequest request) { + return request.getElements().stream() + .filter(AmqpMessageDispatcherServiceIntegrationTest::isDownloadAndUpdateRequest) + .map(multiAction -> (DmfDownloadAndUpdateRequest) multiAction.getAction()).collect(Collectors.toList()); + } + + private static boolean isDownloadAndUpdateRequest(final DmfMultiActionElement multiActionElement) { + return multiActionElement.getTopic().equals(EventTopic.DOWNLOAD) + || multiActionElement.getTopic().equals(EventTopic.DOWNLOAD_AND_INSTALL); + } + } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java index 41519072f9..4be78fbfa8 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java @@ -104,7 +104,7 @@ public void registerTargets() { @Description("Tests register invalid target withy empty controller id. Tests register invalid target with null controller id") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) }) public void registerEmptyTarget() { - createAndSendTarget("", TENANT_EXIST); + createAndSendThingCreated("", TENANT_EXIST); assertAllTargetsCount(0); verifyOneDeadLetterMessage(); @@ -114,7 +114,7 @@ public void registerEmptyTarget() { @Description("Tests register invalid target with whitspace controller id. Tests register invalid target with null controller id") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) }) public void registerWhitespaceTarget() { - createAndSendTarget("Invalid Invalid", TENANT_EXIST); + createAndSendThingCreated("Invalid Invalid", TENANT_EXIST); assertAllTargetsCount(0); verifyOneDeadLetterMessage(); @@ -124,7 +124,7 @@ public void registerWhitespaceTarget() { @Description("Tests register invalid target with null controller id. Tests register invalid target with null controller id") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) }) public void registerInvalidNullTargets() { - createAndSendTarget(null, TENANT_EXIST); + createAndSendThingCreated(null, TENANT_EXIST); assertAllTargetsCount(0); verifyOneDeadLetterMessage(); @@ -571,13 +571,13 @@ public void receiveCancelUpdateMessageAfterAssignmentWasCanceled() { final DistributionSet distributionSet = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); final DistributionSetAssignmentResult distributionSetAssignmentResult = assignDistributionSet( distributionSet.getId(), controllerId); - deploymentManagement.cancelAction(distributionSetAssignmentResult.getActions().get(0)); + deploymentManagement.cancelAction(distributionSetAssignmentResult.getActionIds().get(0)); // test registerSameTargetAndAssertBasedOnVersion(controllerId, 1, TargetUpdateStatus.PENDING); // verify - assertCancelActionMessage(distributionSetAssignmentResult.getActions().get(0), controllerId); + assertCancelActionMessage(distributionSetAssignmentResult.getActionIds().get(0), controllerId); Mockito.verifyZeroInteractions(getDeadletterListener()); } @@ -908,7 +908,7 @@ private void sendUpdateAttributesMessageWithGivenAttributes(final String target, private Long registerTargetAndSendActionStatus(final DmfActionStatus sendActionStatus, final String controllerId) { final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId); - final Long actionId = assignmentResult.getActions().get(0); + final Long actionId = assignmentResult.getActionIds().get(0); sendActionUpdateStatus(new DmfActionUpdateStatus(actionId, sendActionStatus)); return actionId; } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/listener/ReplyToListener.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/listener/ReplyToListener.java index 6218789c21..a16bcabe40 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/listener/ReplyToListener.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/listener/ReplyToListener.java @@ -10,8 +10,11 @@ import static org.junit.Assert.fail; +import java.util.ArrayList; +import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; @@ -26,21 +29,26 @@ public class ReplyToListener implements TestRabbitListener { public static final String LISTENER_ID = "replyto"; public static final String REPLY_TO_QUEUE = "reply_queue"; - private final Map eventTopicMessages = new EnumMap<>(EventTopic.class); + private final Map> eventMessages = new EnumMap<>(EventTopic.class); + private final List eventMessageTopics = new ArrayList<>(); private final Map deleteMessages = new HashMap<>(); private final Map pingResponseMessages = new HashMap<>(); @Override @RabbitListener(id = LISTENER_ID, queues = REPLY_TO_QUEUE) public void handleMessage(final Message message) { - final MessageType messageType = MessageType .valueOf(message.getMessageProperties().getHeaders().get(MessageHeaderKey.TYPE).toString()); if (messageType == MessageType.EVENT) { final EventTopic eventTopic = EventTopic .valueOf(message.getMessageProperties().getHeaders().get(MessageHeaderKey.TOPIC).toString()); - eventTopicMessages.put(eventTopic, message); + eventMessageTopics.add(eventTopic); + eventMessages.merge(eventTopic, Collections.singletonList(message), (oldList, listToAdd) -> { + final List newList = new ArrayList<>(oldList); + newList.addAll(listToAdd); + return newList; + }); return; } @@ -62,8 +70,28 @@ public void handleMessage(final Message message) { } - public Map getEventTopicMessages() { - return eventTopicMessages; + public void purge() { + eventMessages.clear(); + deleteMessages.clear(); + pingResponseMessages.clear(); + eventMessageTopics.clear(); + } + + public List getLatestEventMessageTopics() { + return eventMessageTopics; + } + + public void resetLatestEventMessageTopics() { + eventMessageTopics.clear(); + } + + public Message getLatestEventMessage(final EventTopic eventTopic) { + final List messages = getEventMessages().get(eventTopic); + return messages == null ? null : messages.get(messages.size() - 1); + } + + public Map> getEventMessages() { + return eventMessages; } public Map getDeleteMessages() { diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/EventTopic.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/EventTopic.java index 29bb5bd752..123f564ae2 100644 --- a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/EventTopic.java +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/EventTopic.java @@ -17,25 +17,35 @@ public enum EventTopic { * Topic when sending and receiving a update status. */ UPDATE_ACTION_STATUS, + /** * Topic when sending and receiving a download and install task. */ DOWNLOAD_AND_INSTALL, + /** * Topic when sending and receiving a cancel download task. */ CANCEL_DOWNLOAD, + /** * Topic when updating device attributes. */ UPDATE_ATTRIBUTES, + /** * Topic when sending a download only task, skipping the install. */ DOWNLOAD, + /** * Topic when an update of device attributes is requested. */ - REQUEST_ATTRIBUTES_UPDATE; + REQUEST_ATTRIBUTES_UPDATE, + + /** + * Topic to send multiple actions to the device. + */ + MULTI_ACTION; } diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfMultiActionRequest.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfMultiActionRequest.java new file mode 100644 index 0000000000..9f98d331cd --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfMultiActionRequest.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2019 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.json.model; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * JSON representation of a multi-action request. + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class DmfMultiActionRequest { + + private List elements; + + public DmfMultiActionRequest() { + } + + @JsonCreator + public DmfMultiActionRequest(final List elements) { + this.elements = elements; + } + + @JsonValue + public List getElements() { + return elements; + } + + public void addElement(final DmfMultiActionElement element) { + if (elements == null) { + elements = new ArrayList<>(); + } + elements.add(element); + } + + public void addElement(final EventTopic topic, final DmfActionRequest action) { + final DmfMultiActionElement element = new DmfMultiActionElement(); + element.setTopic(topic); + element.setAction(action); + addElement(element); + } + + /** + * Represents an element within a {@link DmfMultiActionRequest}. + */ + public static class DmfMultiActionElement { + + @JsonProperty + private EventTopic topic; + + @JsonProperty + private DmfActionRequest action; + + public DmfActionRequest getAction() { + return action; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "topic", defaultImpl = DmfActionRequest.class) + @JsonSubTypes({ @Type(value = DmfDownloadAndUpdateRequest.class, name = "DOWNLOAD"), + @Type(value = DmfDownloadAndUpdateRequest.class, name = "DOWNLOAD_AND_INSTALL") }) + public void setAction(final DmfActionRequest action) { + this.action = action; + } + + public EventTopic getTopic() { + return topic; + } + + public void setTopic(final EventTopic actionType) { + this.topic = actionType; + } + + } + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java index 6f1910f385..2c982e60b6 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java @@ -151,6 +151,19 @@ Map> findTargetVisibleMetaDataBySoftwareModul @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) Optional findOldestActiveActionByTarget(@NotEmpty String controllerId); + /** + * Retrieves all active actions which are assigned to the target with the + * given controller ID. + * + * @param pageable + * pagination parameter + * @param controllerId + * of the target + * @return the requested {@link Page} with {@link Action}s + */ + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + Page findActiveActionsByTarget(@NotNull Pageable pageable, @NotEmpty String controllerId); + /** * Get the {@link Action} entity for given actionId with all lazy * attributes. 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 7fbb989726..4e76e1b15d 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 @@ -104,6 +104,33 @@ DistributionSetAssignmentResult assignDistributionSet(long dsID, @NotNull Action 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 + * if mandatory {@link SoftwareModuleType} are not assigned as + * defined 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) + List assignDistributionSets(@NotEmpty Set dsIDs, + @NotEmpty Collection targets); + /** * Assigns the addressed {@link DistributionSet} to all {@link Target}s by * their IDs with a specific {@link ActionType} and an action message. diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryConstants.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryConstants.java index 9ec08272d7..b7a9723442 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryConstants.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryConstants.java @@ -32,6 +32,11 @@ public final class RepositoryConstants { */ public static final int DEFAULT_DS_TYPES_IN_TENANT = 3; + /** + * Maximum number of actions that can be retrieved. + */ + public static final int MAX_ACTION_COUNT = 100; + /** * Maximum number of messages that can be retrieved by a controller for an * {@link Action}. diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/MultiActionEvent.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/MultiActionEvent.java new file mode 100644 index 0000000000..e049d03574 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/MultiActionEvent.java @@ -0,0 +1,58 @@ +/** + * 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.event.remote; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Generic deployment event for the Multi-Assignments feature. The event payload + * holds a list of controller IDs identifying the targets which are affected by + * a deployment action (e.g. a software assignment (update) or a cancellation of + * an update). + */ +public class MultiActionEvent extends RemoteTenantAwareEvent implements Iterable { + + private static final long serialVersionUID = 1L; + + private final List controllerIds = new ArrayList<>(); + + /** + * Default constructor. + */ + public MultiActionEvent() { + // for serialization libs like jackson + } + + /** + * Constructor. + * + * @param tenant + * tenant the event is scoped to + * @param applicationId + * the application id + * @param controllerIds + * the controller IDs of the affected targets + */ + public MultiActionEvent(final String tenant, final String applicationId, final List controllerIds) { + super(applicationId, tenant, applicationId); + this.controllerIds.addAll(controllerIds); + } + + public List getControllerIds() { + return controllerIds; + } + + @Override + public Iterator iterator() { + return controllerIds.iterator(); + } + +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/TenantConfigurationValueChangeNotAllowedException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/TenantConfigurationValueChangeNotAllowedException.java new file mode 100644 index 0000000000..8471179d1c --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/TenantConfigurationValueChangeNotAllowedException.java @@ -0,0 +1,43 @@ +/** + * 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; + +/** + * Exception which is supposed to be thrown if a property value is valid but + * cannot be set in the current context. + */ +public class TenantConfigurationValueChangeNotAllowedException extends AbstractServerRtException { + + private static final long serialVersionUID = 1L; + + /** + * Creates a new exception for the + * {@link SpServerError#SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED} error + * case. + */ + public TenantConfigurationValueChangeNotAllowedException() { + super(SpServerError.SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED); + } + + /** + * Creates a new exception for the + * {@link SpServerError#SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED} error + * case. + * + * @param message + * A custom error message. + */ + public TenantConfigurationValueChangeNotAllowedException(final String message) { + super(message, SpServerError.SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetAssignmentResult.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetAssignmentResult.java index 350373daf9..e0aa0fc57c 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetAssignmentResult.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetAssignmentResult.java @@ -26,12 +26,16 @@ public class DistributionSetAssignmentResult extends AssignmentResult { private final List assignedTargets; private final List actions; + private final DistributionSet distributionSet; + private final TargetManagement targetManagement; /** * * Constructor. * + * @param distributionSet + * that has been assigned * @param assignedTargets * the target objects which have been assigned to the * distribution set @@ -39,37 +43,53 @@ public class DistributionSetAssignmentResult extends AssignmentResult { * count of the assigned targets * @param alreadyAssigned * the count of the already assigned targets - * @param targetManagement - * to retrieve the assigned targets * @param actions * of the assignment - * + * @param targetManagement + * to retrieve the assigned targets */ - public DistributionSetAssignmentResult(final List assignedTargets, final int assigned, - final int alreadyAssigned, final List actions, final TargetManagement targetManagement) { + public DistributionSetAssignmentResult(final DistributionSet distributionSet, final List assignedTargets, + final int assigned, final int alreadyAssigned, final List actions, + final TargetManagement targetManagement) { super(assigned, alreadyAssigned, 0, Collections.emptyList(), Collections.emptyList()); + this.distributionSet = distributionSet; this.assignedTargets = assignedTargets; this.actions = actions; this.targetManagement = targetManagement; } + /** + * @return The distribution set that has been assigned + */ + public DistributionSet getDistributionSet() { + return distributionSet; + } + /** * @return the actionIds */ - public List getActions() { + public List getActionIds() { if (actions == null) { return Collections.emptyList(); } - return actions.stream().map(Action::getId).collect(Collectors.toList()); } + /** + * @return the actions + */ + public List getActions() { + if (actions == null) { + return Collections.emptyList(); + } + return actions; + } + @Override public List getAssignedEntity() { if (CollectionUtils.isEmpty(assignedTargets)) { return Collections.emptyList(); } - return targetManagement.getByControllerID(assignedTargets); } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TenantConfigurationValue.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TenantConfigurationValue.java index 5f27357cec..dbb242feca 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TenantConfigurationValue.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TenantConfigurationValue.java @@ -11,7 +11,7 @@ import java.io.Serializable; /** - * represents a tenant configuration value including some meta data + * Represents a tenant configuration value including some meta data * * @param * type of the configuration value diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java index dab824d7ba..628b67f914 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java @@ -142,6 +142,11 @@ public static class TenantConfigurationKey { */ public static final String ACTION_CLEANUP_ACTION_STATUS = "action.cleanup.actionStatus"; + /** + * Switch to enable/disable the multi-assignment feature. + */ + public static final String MULTI_ASSIGNMENTS_ENABLED = "multi.assignments.enabled"; + private String keyName; private String defaultValue = ""; private Class dataType = String.class; diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/event/EventType.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/event/EventType.java index fe904aedd5..49fba4bcb8 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/event/EventType.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/event/EventType.java @@ -17,6 +17,7 @@ import org.eclipse.hawkbit.repository.event.remote.DistributionSetTagDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.DistributionSetTypeDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent; +import org.eclipse.hawkbit.repository.event.remote.MultiActionEvent; import org.eclipse.hawkbit.repository.event.remote.RolloutDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.RolloutGroupDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.SoftwareModuleDeletedEvent; @@ -132,6 +133,9 @@ public class EventType { // target attributes requested flag TYPES.put(37, TargetAttributesRequestedEvent.class); + + // deployment event for assignments and /or cancellations + TYPES.put(38, MultiActionEvent.class); } private int value; diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties b/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties index e363bbd850..8b8d99808c 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties +++ b/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties @@ -91,4 +91,10 @@ hawkbit.server.tenant.configuration.action-cleanup-action-expiry.validator=org.e hawkbit.server.tenant.configuration.action-cleanup-action-status.keyName=action.cleanup.actionStatus hawkbit.server.tenant.configuration.action-cleanup-action-status.defaultValue=CANCELED,ERROR +hawkbit.server.tenant.configuration.multi-assignments-enabled.keyName=multi.assignments.enabled +hawkbit.server.tenant.configuration.multi-assignments-enabled.defaultValue=false +hawkbit.server.tenant.configuration.multi-assignments-enabled.dataType=java.lang.Boolean +hawkbit.server.tenant.configuration.multi-assignments-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator + + # Default tenant configuration - END 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 a88c9adbd5..4bac7849a8 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 @@ -17,7 +17,6 @@ import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; -import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.entity.CancelTargetAssignmentEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; @@ -30,13 +29,13 @@ import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.bus.BusProperties; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.util.CollectionUtils; /** * {@link DistributionSet} to {@link Target} assignment strategy as utility for @@ -51,7 +50,7 @@ public abstract class AbstractDsAssignmentStrategy { protected final AfterTransactionCommitExecutor afterCommit; protected final ApplicationEventPublisher eventPublisher; protected final BusProperties bus; - private final ActionRepository actionRepository; + protected final ActionRepository actionRepository; private final ActionStatusRepository actionStatusRepository; private final QuotaManagement quotaManagement; @@ -79,6 +78,13 @@ public abstract class AbstractDsAssignmentStrategy { */ abstract List findTargetsForAssignment(final List controllerIDs, final long distributionSetId); + /** + * + * @param set + * @param targets + */ + abstract void sendTargetUpdatedEvents(final DistributionSet set, final List targets); + /** * Update status and DS fields of given target. * @@ -89,8 +95,8 @@ public abstract class AbstractDsAssignmentStrategy { * @param currentUser * for auditing */ - abstract void updateTargetStatus(final JpaDistributionSet distributionSet, final List> targetIds, - final String currentUser); + abstract void setAssignedDistributionSetAndTargetStatus(final JpaDistributionSet distributionSet, + final List> targetIds, final String currentUser); /** * Cancels actions that can be canceled (i.e. @@ -112,35 +118,12 @@ abstract void updateTargetStatus(final JpaDistributionSet distributionSet, final * * @param targetIds * to cancel actions for - * @return {@link Set} of {@link Target#getId()}s */ abstract void closeActiveActions(List> targetIds); - /** - * Handles event sending related to the assignment. - * - * @param set - * that has been assigned - * @param targets - * to send events for - * @param targetIdsCancelList - * targets where an action was canceled - * @param controllerIdsToActions - * mapping of {@link Target#getControllerId()} to new - * {@link Action} that was created as part of the assignment. - */ - abstract void sendAssignmentEvents(DistributionSet set, final List targets, - final Set targetIdsCancelList, final Map controllerIdsToActions); + abstract void sendDeploymentEvents(final DistributionSetAssignmentResult assignmentResult); - protected void sendTargetAssignDistributionSetEvent(final String tenant, final long distributionSetId, - final List actions) { - if (CollectionUtils.isEmpty(actions)) { - return; - } - - afterCommit.afterCommit(() -> eventPublisher.publishEvent(new TargetAssignDistributionSetEvent(tenant, - distributionSetId, actions, bus.getId(), actions.get(0).isMaintenanceWindowAvailable()))); - } + abstract void sendDeploymentEvents(final List assignmentResults); protected void sendTargetUpdatedEvent(final JpaTarget target) { afterCommit.afterCommit(() -> eventPublisher.publishEvent(new TargetUpdatedEvent(target, bus.getId()))); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java index 14e088c3b5..bc56c0d2ba 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java @@ -358,6 +358,14 @@ public Optional findOldestActiveActionByTarget(final String controllerId true); } + @Override + public Page findActiveActionsByTarget(final Pageable pageable, final String controllerId) { + if (!actionRepository.activeActionExistsForControllerId(controllerId)) { + return Page.empty(); + } + return actionRepository.findByActiveAndTarget(pageable, controllerId, true); + } + @Override public Optional findActionWithDetails(final long actionId) { return actionRepository.getById(actionId); @@ -641,7 +649,7 @@ private String handleDownloadedActionStatus(final JpaAction action) { return null; } - JpaTarget target = (JpaTarget) action.getTarget(); + final JpaTarget target = (JpaTarget) action.getTarget(); action.setActive(false); action.setStatus(DOWNLOADED); target.setUpdateStatus(TargetUpdateStatus.IN_SYNC); 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 3a154b338d..452abfb090 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java @@ -8,6 +8,10 @@ */ package org.eclipse.hawkbit.repository.jpa; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED; + +import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -66,7 +70,6 @@ import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; -import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.bus.BusProperties; @@ -85,7 +88,6 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.CollectionUtils; import org.springframework.validation.annotation.Validated; import com.google.common.collect.Lists; @@ -123,9 +125,6 @@ public class JpaDeploymentManagement implements DeploymentManagement { private final ActionStatusRepository actionStatusRepository; private final TargetManagement targetManagement; private final AuditorAware auditorProvider; - private final ApplicationEventPublisher eventPublisher; - private final BusProperties bus; - private final AfterTransactionCommitExecutor afterCommit; private final VirtualPropertyReplacer virtualPropertyReplacer; private final PlatformTransactionManager txManager; private final OnlineDsAssignmentStrategy onlineDsAssignmentStrategy; @@ -151,13 +150,10 @@ protected JpaDeploymentManagement(final EntityManager entityManager, final Actio this.actionStatusRepository = actionStatusRepository; this.targetManagement = targetManagement; this.auditorProvider = auditorProvider; - this.eventPublisher = eventPublisher; - this.bus = bus; - this.afterCommit = afterCommit; this.virtualPropertyReplacer = virtualPropertyReplacer; this.txManager = txManager; onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisher, bus, - actionRepository, actionStatusRepository, quotaManagement); + actionRepository, actionStatusRepository, quotaManagement, this::isMultiAssignmentsEnabled); offlineDsAssignmentStrategy = new OfflineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisher, bus, actionRepository, actionStatusRepository, quotaManagement); this.tenantConfigurationManagement = tenantConfigurationManagement; @@ -173,11 +169,13 @@ protected JpaDeploymentManagement(final EntityManager entityManager, final Actio ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public DistributionSetAssignmentResult offlineAssignedDistributionSet(final Long dsID, final Collection controllerIDs) { - return assignDistributionSetToTargets(dsID, + 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 @@ -187,12 +185,13 @@ public DistributionSetAssignmentResult offlineAssignedDistributionSet(final Long public DistributionSetAssignmentResult assignDistributionSet(final long dsID, final ActionType actionType, final long forcedTimestamp, final Collection controllerIDs) { - return assignDistributionSetToTargets(dsID, + 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 @@ -202,7 +201,21 @@ public DistributionSetAssignmentResult assignDistributionSet(final long dsID, fi public DistributionSetAssignmentResult assignDistributionSet(final long dsID, final Collection targets) { - return assignDistributionSetToTargets(dsID, targets, null, onlineDsAssignmentStrategy); + 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)) + .collect(Collectors.toList()); + onlineDsAssignmentStrategy.sendDeploymentEvents(results); + return results; } @Override @@ -212,7 +225,10 @@ public DistributionSetAssignmentResult assignDistributionSet(final long dsID, public DistributionSetAssignmentResult assignDistributionSet(final long dsID, final Collection targets, final String actionMessage) { - return assignDistributionSetToTargets(dsID, targets, actionMessage, onlineDsAssignmentStrategy); + final DistributionSetAssignmentResult result = assignDistributionSetToTargets(dsID, targets, actionMessage, + onlineDsAssignmentStrategy); + onlineDsAssignmentStrategy.sendDeploymentEvents(result); + return result; } /** @@ -257,8 +273,8 @@ private DistributionSetAssignmentResult assignDistributionSetToTargets(final Lon // detaching as it is not necessary to persist the set itself entityManager.detach(distributionSetEntity); // return with nothing as all targets had the DS already assigned - return new DistributionSetAssignmentResult(Collections.emptyList(), 0, targetsWithActionType.size(), - Collections.emptyList(), targetManagement); + return new DistributionSetAssignmentResult(distributionSetEntity, Collections.emptyList(), 0, + targetsWithActionType.size(), Collections.emptyList(), targetManagement); } // split tIDs length into max entries in-statement because many database @@ -269,12 +285,7 @@ private DistributionSetAssignmentResult assignDistributionSetToTargets(final Lon targetEntities.stream().map(Target::getId).collect(Collectors.toList()), Constants.MAX_ENTRIES_IN_STATEMENT); - // override all active actions and set them into canceling state, we - // need to remember which one we have been switched to canceling state - // because for targets which we have changed to canceling we don't want - // to publish the new action update event. - final Set cancelingTargetEntitiesIds = closeOrCancelActiveActions(assignmentStrategy, - targetEntitiesIdsChunks); + 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 @@ -291,10 +302,9 @@ private DistributionSetAssignmentResult assignDistributionSetToTargets(final Lon // action history. createActionsStatus(controllerIdsToActions.values(), assignmentStrategy, actionMessage); - detachEntitiesAndSendAssignmentEvents(distributionSetEntity, targetEntities, assignmentStrategy, - cancelingTargetEntitiesIds, controllerIdsToActions); + detachEntitiesAndSendTargetUpdatedEvents(distributionSetEntity, targetEntities, assignmentStrategy); - return new DistributionSetAssignmentResult( + return new DistributionSetAssignmentResult(distributionSetEntity, targetEntities.stream().map(Target::getControllerId).collect(Collectors.toList()), targetEntities.size(), controllerIDs.size() - targetEntities.size(), Lists.newArrayList(controllerIdsToActions.values()), targetManagement); @@ -340,34 +350,37 @@ private void assertMaxTargetsPerManualAssignmentQuota(final Long distributionSet quotaManagement.getMaxTargetsPerManualAssignment(), Target.class, DistributionSet.class, null); } - private Set closeOrCancelActiveActions(final AbstractDsAssignmentStrategy assignmentStrategy, + 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); - return Collections.emptySet(); } else { - return assignmentStrategy.cancelActiveActions(targetIdsChunks); + assignmentStrategy.cancelActiveActions(targetIdsChunks); } } - protected boolean isActionsAutocloseEnabled() { - return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement - .getConfigurationValue(TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED, Boolean.class) - .getValue()); - } - @Override @Transactional(isolation = Isolation.READ_COMMITTED) @Retryable(include = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public void cancelInactiveScheduledActionsForTargets(final List targetIds) { - actionRepository.switchStatus(Status.CANCELED, targetIds, false, Status.SCHEDULED); + if (!isMultiAssignmentsEnabled()) { + actionRepository.switchStatus(Status.CANCELED, targetIds, false, Status.SCHEDULED); + } else { + LOG.debug("The Multi Assignments feature is enabled: No need to cancel inactive scheduled actions."); + } } private void setAssignedDistributionSetAndTargetUpdateStatus(final AbstractDsAssignmentStrategy assignmentStrategy, final JpaDistributionSet set, final List> targetIdsChunks) { final String currentUser = auditorProvider.getCurrentAuditor().orElse(null); - assignmentStrategy.updateTargetStatus(set, targetIdsChunks, currentUser); + assignmentStrategy.setAssignedDistributionSetAndTargetStatus(set, targetIdsChunks, currentUser); } private Map createActions(final Collection targetsWithActionType, @@ -388,15 +401,13 @@ private void createActionsStatus(final Collection actions, .collect(Collectors.toList())); } - private void detachEntitiesAndSendAssignmentEvents(final JpaDistributionSet set, final List targets, - final AbstractDsAssignmentStrategy assignmentStrategy, final Set targetIdsCancellList, - final Map controllerIdsToActions) { + private void detachEntitiesAndSendTargetUpdatedEvents(final JpaDistributionSet set, final List targets, + final AbstractDsAssignmentStrategy assignmentStrategy) { // detaching as it is not necessary to persist the set itself entityManager.detach(set); // detaching as the entity has been updated by the JPQL query above targets.forEach(entityManager::detach); - - assignmentStrategy.sendAssignmentEvents(set, targets, targetIdsCancellList, controllerIdsToActions); + assignmentStrategy.sendTargetUpdatedEvents(set, targets); } @Override @@ -421,7 +432,8 @@ public Action cancelAction(final long actionId) { actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELING, System.currentTimeMillis(), RepositoryConstants.SERVER_MESSAGE_PREFIX + "manual cancelation requested")); final Action saveAction = actionRepository.save(action); - onlineDsAssignmentStrategy.cancelAssignDistributionSetEvent(action.getTarget(), action.getId()); + + onlineDsAssignmentStrategy.cancelAssignment(action); return saveAction; } else { @@ -481,18 +493,13 @@ private long startScheduledActionsByRolloutGroupParentInNewTransaction(final Lon return 0L; } - final String tenant = rolloutGroupActions.getContent().get(0).getTenant(); - final boolean maintenanceWindowAvailable = rolloutGroupActions.getContent().get(0) - .isMaintenanceWindowAvailable(); - final List targetAssignments = rolloutGroupActions.getContent().stream() .map(action -> (JpaAction) action).map(this::closeActionIfSetWasAlreadyAssigned) .filter(Objects::nonNull).map(this::startScheduledActionIfNoCancelationHasToBeHandledFirst) .filter(Objects::nonNull).collect(Collectors.toList()); - if (!CollectionUtils.isEmpty(targetAssignments)) { - afterCommit.afterCommit(() -> eventPublisher.publishEvent(new TargetAssignDistributionSetEvent(tenant, - distributionSetId, targetAssignments, bus.getId(), maintenanceWindowAvailable))); + if (!targetAssignments.isEmpty()) { + onlineDsAssignmentStrategy.sendDeploymentEvents(distributionSetId, targetAssignments); } return rolloutGroupActions.getTotalElements(); @@ -513,11 +520,18 @@ private Page findActionsByRolloutAndRolloutGroupParent(final Long rollou } private JpaAction closeActionIfSetWasAlreadyAssigned(final JpaAction action) { + + if (isMultiAssignmentsEnabled()) { + return action; + } + final JpaTarget target = (JpaTarget) action.getTarget(); if (target.getAssignedDistributionSet() != null && action.getDistributionSet().getId().equals(target.getAssignedDistributionSet().getId())) { // the target has already the distribution set assigned, we don't // need to start the scheduled action, just finish it. + LOG.debug("Target {} has distribution set {} assigned. Closing action...", target.getControllerId(), + action.getDistributionSet().getName()); action.setStatus(Status.FINISHED); action.setActive(false); setSkipActionStatus(action); @@ -532,15 +546,16 @@ private JpaAction startScheduledActionIfNoCancelationHasToBeHandledFirst(final J // check if we need to override running update actions final List overrideObsoleteUpdateActions; - if (systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement - .getConfigurationValue(TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED, Boolean.class) - .getValue())) { + if (isMultiAssignmentsEnabled()) { overrideObsoleteUpdateActions = Collections.emptyList(); - onlineDsAssignmentStrategy - .closeObsoleteUpdateActions(Collections.singletonList(action.getTarget().getId())); } else { - overrideObsoleteUpdateActions = onlineDsAssignmentStrategy - .overrideObsoleteUpdateActions(Collections.singletonList(action.getTarget().getId())); + final List targetId = Collections.singletonList(action.getTarget().getId()); + if (isActionsAutocloseEnabled()) { + overrideObsoleteUpdateActions = Collections.emptyList(); + onlineDsAssignmentStrategy.closeObsoleteUpdateActions(targetId); + } else { + overrideObsoleteUpdateActions = onlineDsAssignmentStrategy.overrideObsoleteUpdateActions(targetId); + } } action.setActive(true); @@ -782,4 +797,18 @@ private static String formatInClause(final Collection elements) { protected ActionRepository getActionRepository() { return actionRepository; } -} + + protected boolean isActionsAutocloseEnabled() { + return getConfigValue(REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED, Boolean.class); + } + + private boolean isMultiAssignmentsEnabled() { + return getConfigValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class); + } + + private T getConfigValue(final String key, final Class valueType) { + return systemSecurityContext + .runAsSystem(() -> tenantConfigurationManagement.getConfigurationValue(key, valueType).getValue()); + } + +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantConfigurationManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantConfigurationManagement.java index 80a7f6f99d..22eec469d2 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantConfigurationManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantConfigurationManagement.java @@ -8,9 +8,13 @@ */ package org.eclipse.hawkbit.repository.jpa; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED; + import java.io.Serializable; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.exception.TenantConfigurationValueChangeNotAllowedException; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.model.JpaTenantConfiguration; import org.eclipse.hawkbit.repository.model.TenantConfiguration; @@ -18,6 +22,8 @@ import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationValidatorException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; @@ -37,6 +43,8 @@ @Validated public class JpaTenantConfigurationManagement implements TenantConfigurationManagement { + private static final Logger LOG = LoggerFactory.getLogger(JpaTenantConfigurationManagement.class); + @Autowired private TenantConfigurationRepository tenantConfigurationRepository; @@ -150,6 +158,8 @@ public TenantConfigurationValue addOrUpdateConfigura tenantConfiguration.setValue(value.toString()); } + assertValueChangeIsAllowed(configurationKeyName, tenantConfiguration); + final JpaTenantConfiguration updatedTenantConfiguration = tenantConfigurationRepository .save(tenantConfiguration); @@ -163,6 +173,41 @@ public TenantConfigurationValue addOrUpdateConfigura .value(conversionService.convert(updatedTenantConfiguration.getValue(), clazzT)).build(); } + /** + * Asserts that the requested configuration value change is allowed. Throws + * a {@link TenantConfigurationValueChangeNotAllowedException} otherwise. + * + * @param configurationKeyName + * The configuration key. + * @param tenantConfiguration + * The configuration to be validated. + * + * @throws TenantConfigurationValueChangeNotAllowedException + * if the requested configuration change is not allowed. + */ + private void assertValueChangeIsAllowed(final String key, final JpaTenantConfiguration valueChange) { + assertMultiAssignmentsValueChange(key, valueChange); + assertAutoCloseValueChange(key, valueChange); + } + + @SuppressWarnings("squid:S1172") + private void assertAutoCloseValueChange(final String key, final JpaTenantConfiguration valueChange) { + if (REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED.equals(key) + && getConfigurationValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class).getValue()) { + LOG.debug( + "The property '{}' must not be changed because the Multi-Assignments feature is currently enabled.", + key); + throw new TenantConfigurationValueChangeNotAllowedException(); + } + } + + private static void assertMultiAssignmentsValueChange(final String key, final JpaTenantConfiguration valueChange) { + if (MULTI_ASSIGNMENTS_ENABLED.equals(key) && !Boolean.parseBoolean(valueChange.getValue())) { + LOG.debug("The Multi-Assignments '{}' feature cannot be disabled.", key); + throw new TenantConfigurationValueChangeNotAllowedException(); + } + } + @Override @CacheEvict(value = "tenantConfiguration", key = "#configurationKeyName") @Transactional 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 af07c185fd..5bcdef8dd8 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 @@ -27,6 +27,7 @@ import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.springframework.cloud.bus.BusProperties; @@ -50,9 +51,7 @@ public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { } @Override - void sendAssignmentEvents(final DistributionSet set, final List targets, - final Set targetIdsCancellList, final Map targetIdsToActions) { - + void sendTargetUpdatedEvents(final DistributionSet set, final List targets) { targets.forEach(target -> { target.setUpdateStatus(TargetUpdateStatus.IN_SYNC); sendTargetUpdatedEvent(target); @@ -79,7 +78,8 @@ void closeActiveActions(final List> targetIds) { } @Override - void updateTargetStatus(final JpaDistributionSet set, final List> targetIds, final String currentUser) { + void setAssignedDistributionSetAndTargetStatus(final JpaDistributionSet set, final List> targetIds, + final String currentUser) { targetIds.forEach(tIds -> targetRepository.setAssignedAndInstalledDistributionSetAndUpdateStatus( TargetUpdateStatus.IN_SYNC, set, System.currentTimeMillis(), currentUser, tIds)); } @@ -103,4 +103,14 @@ protected JpaActionStatus createActionStatus(final JpaAction action, final Strin return result; } + @Override + void sendDeploymentEvents(final DistributionSetAssignmentResult assignmentResult) { + // no need to send deployment events in the offline case + } + + @Override + void sendDeploymentEvents(final List assignmentResults) { + // no need to send deployment events in the offline case + } + } 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 43b73f3dbc..c1d8c886fb 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 @@ -8,13 +8,21 @@ */ package org.eclipse.hawkbit.repository.jpa; +import static org.eclipse.hawkbit.repository.RepositoryConstants.MAX_ACTION_COUNT; + import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.hawkbit.repository.QuotaManagement; +import org.eclipse.hawkbit.repository.event.remote.MultiActionEvent; +import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; @@ -25,11 +33,15 @@ import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.springframework.cloud.bus.BusProperties; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.util.CollectionUtils; import com.google.common.collect.Lists; @@ -39,35 +51,70 @@ */ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { + private static final Pageable ACTION_PAGE_REQUEST = PageRequest.of(0, MAX_ACTION_COUNT); + + private final Supplier multiAssignmentsConfig; + OnlineDsAssignmentStrategy(final TargetRepository targetRepository, final AfterTransactionCommitExecutor afterCommit, final ApplicationEventPublisher eventPublisher, final BusProperties bus, final ActionRepository actionRepository, - final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement) { + final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement, + final Supplier multiAssignmentsConfig) { super(targetRepository, afterCommit, eventPublisher, bus, actionRepository, actionStatusRepository, quotaManagement); + this.multiAssignmentsConfig = multiAssignmentsConfig; } @Override - void sendAssignmentEvents(final DistributionSet set, final List targets, - final Set targetIdsCancellList, final Map targetIdsToActions) { - - final List actions = targets.stream().map(target -> { + void sendTargetUpdatedEvents(final DistributionSet set, final List targets) { + targets.stream().forEach(target -> { target.setUpdateStatus(TargetUpdateStatus.PENDING); sendTargetUpdatedEvent(target); + }); + } - return target; - }).filter(target -> !targetIdsCancellList.contains(target.getId())).map(Target::getControllerId) - .map(targetIdsToActions::get).collect(Collectors.toList()); + @Override + void sendDeploymentEvents(final DistributionSetAssignmentResult assignmentResult) { + if (isMultiAssignmentsEnabled()) { + sendDeploymentEvents(Collections.singletonList(assignmentResult)); + } else { + sendDistributionSetAssignedEvent(assignmentResult); + } + } + + @Override + void sendDeploymentEvents(final List assignmentResults) { + if (isMultiAssignmentsEnabled()) { + sendDeploymentEvent(assignmentResults.stream().flatMap(result -> result.getActions().stream()) + .collect(Collectors.toList())); + } else { + assignmentResults.forEach(this::sendDistributionSetAssignedEvent); + } + } - sendTargetAssignDistributionSetEvent(set.getTenant(), set.getId(), actions); + void sendDeploymentEvents(final long distributionSetId, final List actions) { + if (isMultiAssignmentsEnabled()) { + sendDeploymentEvent(actions); + return; + } + final List filteredActions = getActionsWithoutCancellations(actions); + if (filteredActions.isEmpty()) { + return; + } + sendTargetAssignDistributionSetEvent(filteredActions.get(0).getTenant(), distributionSetId, filteredActions); } @Override List findTargetsForAssignment(final List controllerIDs, final long setId) { - return Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream() - .map(ids -> targetRepository - .findAll(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, setId))) + final Function, List> mapper; + if (isMultiAssignmentsEnabled()) { + mapper = targetRepository::findAllByControllerId; + } else { + mapper = ids -> targetRepository + .findAll(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, setId)); + } + return Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream().map(mapper) .flatMap(List::stream).collect(Collectors.toList()); } @@ -83,7 +130,8 @@ void closeActiveActions(final List> targetIds) { } @Override - void updateTargetStatus(final JpaDistributionSet set, final List> targetIds, final String currentUser) { + void setAssignedDistributionSetAndTargetStatus(final JpaDistributionSet set, final List> targetIds, + final String currentUser) { targetIds.forEach(tIds -> targetRepository.setAssignedDistributionSetAndUpdateStatus(TargetUpdateStatus.PENDING, set, System.currentTimeMillis(), currentUser, tIds)); @@ -106,4 +154,82 @@ JpaActionStatus createActionStatus(final JpaAction action, final String actionMe return result; } + void cancelAssignment(final JpaAction action) { + if (isMultiAssignmentsEnabled()) { + sendMultiActionEvent(action.getTarget()); + } else { + cancelAssignDistributionSetEvent(action.getTarget(), action.getId()); + } + } + + private void sendMultiActionEvent(final Target target) { + sendMultiActionEvent(target.getTenant(), Collections.singletonList(target.getControllerId())); + } + + private void sendDeploymentEvent(final List actions) { + final List filteredActions = getActionsWithoutCancellations(actions); + if (filteredActions.isEmpty()) { + return; + } + final String tenant = filteredActions.get(0).getTenant(); + sendMultiActionEvent(tenant, filteredActions.stream().map(action -> action.getTarget().getControllerId()) + .collect(Collectors.toList())); + } + + private DistributionSetAssignmentResult sendDistributionSetAssignedEvent( + final DistributionSetAssignmentResult assignmentResult) { + final List filteredActions = filterCancellations(assignmentResult.getActions()) + .filter(action -> !hasPendingCancellations(action.getTarget())).collect(Collectors.toList()); + final DistributionSet set = assignmentResult.getDistributionSet(); + sendTargetAssignDistributionSetEvent(set.getTenant(), set.getId(), filteredActions); + return assignmentResult; + } + + private void sendTargetAssignDistributionSetEvent(final String tenant, final long distributionSetId, + final List actions) { + if (CollectionUtils.isEmpty(actions)) { + return; + } + + afterCommit.afterCommit(() -> eventPublisher.publishEvent(new TargetAssignDistributionSetEvent(tenant, + distributionSetId, actions, bus.getId(), actions.get(0).isMaintenanceWindowAvailable()))); + } + + private boolean hasPendingCancellations(final Target target) { + return actionRepository.findByActiveAndTarget(ACTION_PAGE_REQUEST, target.getControllerId(), true).getContent() + .stream().anyMatch(action -> action.getStatus() == Status.CANCELING); + } + + /** + * Helper to fire a {@link MultiActionEvent}. This method may only be called + * if the Multi-Assignments feature is enabled. + * + * @param tenant + * the event is scoped to + * @param controllerIds + * of the targets the event refers to + */ + private void sendMultiActionEvent(final String tenant, final List controllerIds) { + afterCommit.afterCommit( + () -> eventPublisher.publishEvent(new MultiActionEvent(tenant, bus.getId(), controllerIds))); + } + + private boolean isMultiAssignmentsEnabled() { + return multiAssignmentsConfig.get(); + } + + private static Stream filterCancellations(final List actions) { + return actions.stream().filter(action -> { + final Status actionStatus = action.getStatus(); + return Status.CANCELING != actionStatus && Status.CANCELED != actionStatus; + }); + } + + private static List getActionsWithoutCancellations(final List actions) { + if (actions == null || actions.isEmpty()) { + return Collections.emptyList(); + } + return filterCancellations(actions).collect(Collectors.toList()); + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java index 6bc0d2dac2..1f9caf620e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java @@ -223,6 +223,17 @@ List findByTagNameAndControllerIdIn(@Param("tagname") String tag, @Query("SELECT t FROM JpaTarget t WHERE t.id IN ?1") List findAllById(Iterable ids); + /** + * Finds all targets for the given list of controller IDs. + * + * @param controllerIds + * The controller IDs to look for. + * + * @return The list of matching targets. + */ + @Query("SELECT t FROM JpaTarget t WHERE t.controllerId IN ?1") + List findAllByControllerId(Iterable controllerIds); + /** * * Finds all targets of a rollout group. 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 cbbac057ef..4218af521d 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 @@ -939,7 +939,7 @@ public void findMessagesByActionStatusId() { final DistributionSet testDs = testdataFactory.createDistributionSet("1"); final List testTarget = testdataFactory.createTargets(1); - final Long actionId = assignDistributionSet(testDs, testTarget).getActions().get(0); + final Long actionId = assignDistributionSet(testDs, testTarget).getActionIds().get(0); controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId) .status(Action.Status.RUNNING).messages(Lists.newArrayList("proceeding message 1"))); @@ -968,7 +968,7 @@ public void addActionStatusUpdatesUntilQuotaIsExceeded() { // test for informational status final Long actionId1 = assignDistributionSet(testdataFactory.createDistributionSet("ds1"), - testdataFactory.createTargets(1, "t1")).getActions().get(0); + testdataFactory.createTargets(1, "t1")).getActionIds().get(0); assertThat(actionId1).isNotNull(); for (int i = 0; i < maxStatusEntries; ++i) { controllerManagement.addInformationalActionStatus(entityFactory.actionStatus().create(actionId1) @@ -979,7 +979,7 @@ public void addActionStatusUpdatesUntilQuotaIsExceeded() { // test for update status (and mixed case) final Long actionId2 = assignDistributionSet(testdataFactory.createDistributionSet("ds2"), - testdataFactory.createTargets(1, "t2")).getActions().get(0); + testdataFactory.createTargets(1, "t2")).getActionIds().get(0); assertThat(actionId2).isNotEqualTo(actionId1); for (int i = 0; i < maxStatusEntries; ++i) { controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId2) @@ -1002,7 +1002,7 @@ public void createActionStatusWithTooManyMessages() { final int maxMessages = quotaManagement.getMaxMessagesPerActionStatus(); final Long actionId = assignDistributionSet(testdataFactory.createDistributionSet("ds1"), - testdataFactory.createTargets(1)).getActions().get(0); + testdataFactory.createTargets(1)).getActionIds().get(0); assertThat(actionId).isNotNull(); final List messages = Lists.newArrayList(); @@ -1158,19 +1158,22 @@ public void quotaEceededExceptionWhenControllerReportsTooManyUpdateActionStatusM IntStream.range(0, maxMessages).forEach(i -> controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED))); fail("No QuotaExceededException thrown for too many DOWNLOADED updateActionStatus updates"); - } catch (QuotaExceededException e) { } + } catch (final QuotaExceededException e) { + } try { IntStream.range(0, maxMessages).forEach(i -> controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.ERROR))); fail("No QuotaExceededException thrown for too many ERROR updateActionStatus updates"); - } catch (QuotaExceededException e) { } + } catch (final QuotaExceededException e) { + } try { IntStream.range(0, maxMessages).forEach(i -> controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.FINISHED))); fail("No QuotaExceededException thrown for too many FINISHED updateActionStatus updates"); - } catch (QuotaExceededException e) { } + } catch (final QuotaExceededException e) { + } } @Test @@ -1190,19 +1193,22 @@ public void quotaEceededExceptionWhenControllerReportsTooManyUpdateActionStatusM IntStream.range(0, maxMessages).forEach(i -> controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED))); fail("No QuotaExceededException thrown for too many DOWNLOADED updateActionStatus updates"); - } catch (QuotaExceededException e) { } + } catch (final QuotaExceededException e) { + } try { IntStream.range(0, maxMessages).forEach(i -> controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.ERROR))); fail("No QuotaExceededException thrown for too many ERROR updateActionStatus updates"); - } catch (QuotaExceededException e) { } + } catch (final QuotaExceededException e) { + } try { IntStream.range(0, maxMessages).forEach(i -> controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.FINISHED))); fail("No QuotaExceededException thrown for too many FINISHED updateActionStatus updates"); - } catch (QuotaExceededException e) { } + } catch (final QuotaExceededException e) { + } } @Test @@ -1286,7 +1292,7 @@ public void controllerReportsFinishedForOldDownloadOnlyActionAfterSuccessfulForc final Long forcedDistributionSetId = testdataFactory.createDistributionSet("forcedDs1").getId(); final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(forcedDistributionSetId, DEFAULT_CONTROLLER_ID, Action.ActionType.SOFT); - addUpdateActionStatus(assignmentResult.getActions().get(0), DEFAULT_CONTROLLER_ID, Status.FINISHED); + addUpdateActionStatus(assignmentResult.getActions().get(0).getId(), DEFAULT_CONTROLLER_ID, Status.FINISHED); assertAssignedDistributionSetId(DEFAULT_CONTROLLER_ID, forcedDistributionSetId); assertInstalledDistributionSetId(DEFAULT_CONTROLLER_ID, forcedDistributionSetId); assertNoActiveActionsExistsForControllerId(DEFAULT_CONTROLLER_ID); @@ -1326,4 +1332,4 @@ private void assertNoActiveActionsExistsForControllerId(final String controllerI assertThat(actionRepository.activeActionExistsForControllerId(controllerId)).isEqualTo(false); } -} +} \ No newline at end of file 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 58de216b48..eb64b1ed11 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 @@ -25,6 +25,7 @@ import java.util.stream.IntStream; import org.eclipse.hawkbit.repository.ActionStatusFields; +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; import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent; @@ -85,6 +86,10 @@ @Feature("Component Tests - Repository") @Story("Deployment Management") public class DeploymentManagementTest extends AbstractJpaIntegrationTest { + + private static final boolean STATE_ACTIVE = true; + private static final boolean STATE_INACTIVE = false; + private EventHandlerStub eventHandlerStub; private CancelEventHandlerStub cancelEventHandlerStub; @@ -146,7 +151,7 @@ public void findActionWithLazyDetails() { new ArrayList()); final List testTarget = testdataFactory.createTargets(1); // one action with one action status is generated - final Long actionId = assignDistributionSet(testDs, testTarget).getActions().get(0); + final Long actionId = assignDistributionSet(testDs, testTarget).getActionIds().get(0); final Action action = deploymentManagement.findActionWithDetails(actionId).get(); assertThat(action.getDistributionSet()).as("DistributionSet in action").isNotNull(); @@ -163,7 +168,7 @@ public void findActionByTargetId() { new ArrayList()); final List testTarget = testdataFactory.createTargets(1); // one action with one action status is generated - final Long actionId = assignDistributionSet(testDs, testTarget).getActions().get(0); + final Long actionId = assignDistributionSet(testDs, testTarget).getActionIds().get(0); // act final Slice actions = deploymentManagement.findActionsByTarget(testTarget.get(0).getControllerId(), @@ -212,7 +217,7 @@ public void findActionStatusByActionId() { new ArrayList()); final List testTarget = testdataFactory.createTargets(1); // one action with one action status is generated - final Long actionId = assignDistributionSet(testDs, testTarget).getActions().get(0); + final Long actionId = assignDistributionSet(testDs, testTarget).getActionIds().get(0); final Slice actions = deploymentManagement.findActionsByTarget(testTarget.get(0).getControllerId(), PAGE); final ActionStatus expectedActionStatus = ((JpaAction) actions.getContent().get(0)).getActionStatus().get(0); @@ -231,7 +236,7 @@ public void findMessagesByActionStatusId() { new ArrayList()); final List testTarget = testdataFactory.createTargets(1); // one action with one action status is generated - final Long actionId = assignDistributionSet(testDs, testTarget).getActions().get(0); + final Long actionId = assignDistributionSet(testDs, testTarget).getActionIds().get(0); // create action-status entry with one message controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId) .status(Action.Status.FINISHED).messages(Arrays.asList("finished message"))); @@ -485,8 +490,8 @@ public void assignedDistributionSet() { final List targets = deploymentManagement.offlineAssignedDistributionSet(ds.getId(), controllerIds) .getAssignedEntity(); assertThat(actionRepository.count()).isEqualTo(20); - assertThat(actionRepository.findByDistributionSetId(PAGE, ds.getId())) - .as("Offline actions are not active").allMatch(action -> !action.isActive()); + assertThat(actionRepository.findByDistributionSetId(PAGE, ds.getId())).as("Offline actions are not active") + .allMatch(action -> !action.isActive()); assertThat(targetManagement.findByInstalledDistributionSet(PAGE, ds.getId()).getContent()).containsAll(targets) .hasSize(10).containsAll(targetManagement.findByAssignedDistributionSet(PAGE, ds.getId())) .as("InstallationDate set").allMatch(target -> target.getInstallationDate() >= current) @@ -515,26 +520,14 @@ public void assignDistributionSetAndAutoCloseActiveActions() { final DistributionSet ds1 = testdataFactory.createDistributionSet("1"); assignDistributionSet(ds1, targets); - List assignmentOne = actionRepository.findByDistributionSetId(PAGE, ds1.getId()).getContent(); - assertThat(assignmentOne).hasSize(10).as("Is active").allMatch(Action::isActive) - .as("Is assigned to DS " + ds1.getId()) - .allMatch(action -> action.getDistributionSet().getId().equals(ds1.getId())).as("Is running") - .allMatch(action -> action.getStatus() == Status.RUNNING); + assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING); // Second assignment final DistributionSet ds2 = testdataFactory.createDistributionSet("2"); assignDistributionSet(ds2, targets); - final List assignmentTwo = actionRepository.findByDistributionSetId(PAGE, ds2.getId()).getContent(); - assignmentOne = actionRepository.findByDistributionSetId(PAGE, ds1.getId()).getContent(); - assertThat(assignmentTwo).hasSize(10).as("Is active").allMatch(Action::isActive) - .as("Is assigned to DS " + ds2.getId()) - .allMatch(action -> action.getDistributionSet().getId().equals(ds2.getId())).as("Is running") - .allMatch(action -> action.getStatus() == Status.RUNNING); - assertThat(assignmentOne).hasSize(10).as("Is active").allMatch(action -> !action.isActive()) - .as("Is assigned to DS " + ds1.getId()) - .allMatch(action -> action.getDistributionSet().getId().equals(ds1.getId())).as("Is cancelled") - .allMatch(action -> action.getStatus() == Status.CANCELED); + assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, Status.RUNNING); + assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_INACTIVE, Status.CANCELED); assertThat(targetManagement.findByAssignedDistributionSet(PAGE, ds2.getId()).getContent()).hasSize(10) .as("InstallationDate not set").allMatch(target -> (target.getInstallationDate() == null)); @@ -545,6 +538,44 @@ public void assignDistributionSetAndAutoCloseActiveActions() { } } + @Test + @Description("If multi-assignment is enabled, verify that the previous Distribution Set assignment is not canceled when a new one is assigned.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 10), + @Expect(type = TargetUpdatedEvent.class, count = 20), @Expect(type = ActionCreatedEvent.class, count = 20), + @Expect(type = DistributionSetCreatedEvent.class, count = 2), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6), + @Expect(type = MultiActionEvent.class, count = 2), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 0) }) + public void previousAssignmentsAreNotCanceledInMultiAssignMode() { + enableMultiAssignments(); + final List targets = testdataFactory.createTargets(10); + + // First assignment + final DistributionSet ds1 = testdataFactory.createDistributionSet("Multi-assign-1"); + assignDistributionSet(ds1, targets); + + assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING); + + // Second assignment + final DistributionSet ds2 = testdataFactory.createDistributionSet("Multi-assign-2"); + assignDistributionSet(ds2, targets); + + assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, Status.RUNNING); + assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING); + } + + private void assertDsExclusivelyAssignedToTargets(final List targets, final long dsId, final boolean active, + final Status status) { + final List assignment = actionRepository.findByDistributionSetId(PAGE, dsId).getContent(); + + assertThat(assignment).hasSize(10).allMatch(action -> action.isActive() == active) + .as("Is assigned to DS " + dsId).allMatch(action -> action.getDistributionSet().getId().equals(dsId)) + .as("State is " + status).allMatch(action -> action.getStatus() == status); + final long[] targetIds = targets.stream().mapToLong(Target::getId).toArray(); + assertThat(targetIds).as("All targets represented in assignment").containsExactlyInAnyOrder( + assignment.stream().mapToLong(action -> action.getTarget().getId()).toArray()); + } + /** * test a simple deployment by calling the * {@link TargetRepository#assignDistributionSet(DistributionSet, Iterable)} @@ -992,7 +1023,7 @@ public void forceSoftAction() { ds.getId(), ActionType.SOFT, org.eclipse.hawkbit.repository.model.RepositoryModelConstants.NO_FORCE_TIME, Arrays.asList(target.getControllerId())); - final Long actionId = assignDistributionSet.getActions().get(0); + final Long actionId = assignDistributionSet.getActionIds().get(0); // verify preparation Action findAction = deploymentManagement.findAction(actionId).get(); assertThat(findAction.getActionType()).as("action type is wrong").isEqualTo(ActionType.SOFT); @@ -1016,7 +1047,7 @@ public void forceAlreadyForcedActionNothingChanges() { ds.getId(), ActionType.FORCED, org.eclipse.hawkbit.repository.model.RepositoryModelConstants.NO_FORCE_TIME, Arrays.asList(target.getControllerId())); - final Long actionId = assignDistributionSet.getActions().get(0); + final Long actionId = assignDistributionSet.getActionIds().get(0); // verify perparation Action findAction = deploymentManagement.findAction(actionId).get(); assertThat(findAction.getActionType()).as("action type is wrong").isEqualTo(ActionType.FORCED); @@ -1087,14 +1118,12 @@ private void assertTargetAssignDistributionSetEvents(final List targets, assertThat(event).isNotNull(); assertThat(event.getDistributionSetId()).isEqualTo(ds.getId()); - List eventActionIds = event.getActions().values().stream().map(ActionProperties::getId) + final List eventActionIds = event.getActions().values().stream().map(ActionProperties::getId) .collect(Collectors.toList()); - List targetActiveActionIds = targets.stream() + final List targetActiveActionIds = targets.stream() .map(t -> deploymentManagement.findActiveActionsByTarget(PAGE, t.getControllerId()).getContent()) - .flatMap(List::stream) - .map(Action::getId) - .collect(Collectors.toList()); + .flatMap(List::stream).map(Action::getId).collect(Collectors.toList()); assertThat(eventActionIds).containsOnlyElementsOf(targetActiveActionIds); } @@ -1216,4 +1245,8 @@ public void onApplicationEvent(final CancelTargetAssignmentEvent event) { } } + 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 7eb116df3e..a17190a970 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 @@ -104,7 +104,7 @@ public void rolloutShouldNotCancelRunningActionWithTheSameDistributionSet() { testdataFactory.createTarget(knownControllerId); final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet( knownDistributionSet.getId(), ActionType.FORCED, 0, Collections.singleton(knownControllerId)); - final Long manuallyAssignedActionId = assignmentResult.getActions().get(0); + final Long manuallyAssignedActionId = assignmentResult.getActionIds().get(0); // create rollout with the same distribution set already assigned // start rollout @@ -144,7 +144,7 @@ public void rolloutAssignesNewDistributionSetAndAutoCloseActiveActions() { testdataFactory.createTarget(knownControllerId); final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet( firstDistributionSet.getId(), ActionType.FORCED, 0, Collections.singleton(knownControllerId)); - final Long manuallyAssignedActionId = assignmentResult.getActions().get(0); + final Long manuallyAssignedActionId = assignmentResult.getActionIds().get(0); // create rollout with the same distribution set already assigned // start rollout diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementSearchTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementSearchTest.java index df2c30242f..4a26268e4e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementSearchTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementSearchTest.java @@ -96,7 +96,7 @@ public void targetSearchWithVariousFilterCombinations() { final String assignedB = targBs.iterator().next().getControllerId(); assignDistributionSet(setA.getId(), assignedB); final String installedC = targCs.iterator().next().getControllerId(); - final Long actionId = assignDistributionSet(installedSet.getId(), assignedC).getActions().get(0); + final Long actionId = assignDistributionSet(installedSet.getId(), assignedC).getActionIds().get(0); // add attributes to match against only attribute value or attribute // value and name @@ -701,7 +701,7 @@ public void findTargetByInstalledDistributionSet() { List installedtargets = testdataFactory.createTargets(10, "assigned", "assigned"); // set on installed and assign another one - assignDistributionSet(installedSet, installedtargets).getActions().forEach(actionId -> controllerManagement + assignDistributionSet(installedSet, installedtargets).getActionIds().forEach(actionId -> controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.FINISHED))); assignDistributionSet(assignedSet, installedtargets); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java index e15e62a078..0ef1390f93 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java @@ -477,7 +477,7 @@ public void findTargetByControllerIDWithDetails() { final DistributionSetAssignmentResult result = assignDistributionSet(set.getId(), "4711"); controllerManagement.addUpdateActionStatus( - entityFactory.actionStatus().create(result.getActions().get(0)).status(Status.FINISHED)); + entityFactory.actionStatus().create(result.getActionIds().get(0)).status(Status.FINISHED)); assignDistributionSet(set2.getId(), "4711"); target = targetManagement.getByControllerID("4711").get(); 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 ae230a24f3..b8beb82d0e 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 @@ -59,7 +59,7 @@ public void autoAssignDistributionSetAndAutoCloseOldActions() { testdataFactory.createTarget(knownControllerId); final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet( firstDistributionSet.getId(), ActionType.FORCED, 0, Collections.singleton(knownControllerId)); - final Long manuallyAssignedActionId = assignmentResult.getActions().get(0); + final Long manuallyAssignedActionId = assignmentResult.getActionIds().get(0); // target filter query that matches all targets final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement 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 5d83ec12dd..89c1497b06 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 @@ -81,7 +81,7 @@ public void cleanupDisabled() { final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); final Long action1 = deploymentManagement.assignDistributionSet(ds1.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg1.getControllerId())).getActions().get(0); + Collections.singletonList(trg1.getControllerId())).getActionIds().get(0); deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, Collections.singletonList(trg2.getControllerId())); @@ -110,11 +110,11 @@ public void canceledAndFailedActionsAreCleanedUp() { final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); final Long action1 = deploymentManagement.assignDistributionSet(ds1.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg1.getControllerId())).getActions().get(0); + Collections.singletonList(trg1.getControllerId())).getActionIds().get(0); final Long action2 = deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg2.getControllerId())).getActions().get(0); + Collections.singletonList(trg2.getControllerId())).getActionIds().get(0); final Long action3 = deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg3.getControllerId())).getActions().get(0); + Collections.singletonList(trg3.getControllerId())).getActionIds().get(0); assertThat(actionRepository.count()).isEqualTo(3); @@ -145,11 +145,11 @@ public void canceledActionsAreCleanedUp() { final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); final Long action1 = deploymentManagement.assignDistributionSet(ds1.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg1.getControllerId())).getActions().get(0); + Collections.singletonList(trg1.getControllerId())).getActionIds().get(0); final Long action2 = deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg2.getControllerId())).getActions().get(0); + Collections.singletonList(trg2.getControllerId())).getActionIds().get(0); final Long action3 = deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg3.getControllerId())).getActions().get(0); + Collections.singletonList(trg3.getControllerId())).getActionIds().get(0); assertThat(actionRepository.count()).isEqualTo(3); @@ -182,11 +182,11 @@ public void canceledAndFailedActionsAreCleanedUpWhenExpired() throws Interrupted final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); final Long action1 = deploymentManagement.assignDistributionSet(ds1.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg1.getControllerId())).getActions().get(0); + Collections.singletonList(trg1.getControllerId())).getActionIds().get(0); final Long action2 = deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg2.getControllerId())).getActions().get(0); + Collections.singletonList(trg2.getControllerId())).getActionIds().get(0); final Long action3 = deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, - Collections.singletonList(trg3.getControllerId())).getActions().get(0); + Collections.singletonList(trg3.getControllerId())).getActionIds().get(0); assertThat(actionRepository.count()).isEqualTo(3); diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiCancelActionTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiCancelActionTest.java index 495abe5608..b00769123d 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiCancelActionTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiCancelActionTest.java @@ -35,7 +35,6 @@ import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; import org.springframework.integration.json.JsonPathUtils; -import org.springframework.test.util.JsonPathExpectationsHelper; import io.qameta.allure.Description; import io.qameta.allure.Feature; @@ -52,13 +51,13 @@ public class DdiCancelActionTest extends AbstractDDiApiIntegrationTest { @Description("Tests that the cancel action resource can be used with CBOR.") public void cancelActionCbor() throws Exception { final DistributionSet ds = testdataFactory.createDistributionSet(""); - final Target savedTarget = testdataFactory.createTarget(); - final Long actionId = assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActions() + testdataFactory.createTarget(); + final Long actionId = assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActionIds() .get(0); final Action cancelAction = deploymentManagement.cancelAction(actionId); // check that we can get the cancel action as CBOR - byte[] result = mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/" + final byte[] result = mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/" + cancelAction.getId(), tenantAware.getCurrentTenant()).accept(DdiRestConstants.MEDIA_TYPE_CBOR)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR_UTF8)) @@ -81,7 +80,7 @@ public void rootRsCancelActionButContinueAnyway() throws Exception { final DistributionSet ds = testdataFactory.createDistributionSet(""); final Target savedTarget = testdataFactory.createTarget(); - final Long actionId = assignDistributionSet(ds.getId(), savedTarget.getControllerId()).getActions().get(0); + final Long actionId = assignDistributionSet(ds.getId(), savedTarget.getControllerId()).getActionIds().get(0); final Action cancelAction = deploymentManagement.cancelAction(actionId); @@ -134,7 +133,7 @@ public void rootRsCancelAction() throws Exception { final DistributionSet ds = testdataFactory.createDistributionSet(""); final Target savedTarget = testdataFactory.createTarget(); - final Long actionId = assignDistributionSet(ds.getId(), savedTarget.getControllerId()).getActions().get(0); + final Long actionId = assignDistributionSet(ds.getId(), savedTarget.getControllerId()).getActionIds().get(0); long current = System.currentTimeMillis(); mvc.perform(get("/{tenant}/controller/v1/{controller}", tenantAware.getCurrentTenant(), @@ -250,7 +249,7 @@ private Action createCancelAction(final String targetid) { final Target savedTarget = testdataFactory.createTarget(targetid); final List toAssign = new ArrayList<>(); toAssign.add(savedTarget); - final Long actionId = assignDistributionSet(ds, toAssign).getActions().get(0); + final Long actionId = assignDistributionSet(ds, toAssign).getActionIds().get(0); return deploymentManagement.cancelAction(actionId); } @@ -263,7 +262,7 @@ public void rootRsCancelActionFeedback() throws Exception { final Target savedTarget = testdataFactory.createTarget(); - final Long actionId = assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActions() + final Long actionId = assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActionIds() .get(0); // cancel action manually @@ -338,11 +337,11 @@ public void multipleCancelActionFeedback() throws Exception { final Target savedTarget = testdataFactory.createTarget(); - final Long actionId = assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActions() + final Long actionId = assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActionIds() .get(0); - final Long actionId2 = assignDistributionSet(ds2.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActions() + final Long actionId2 = assignDistributionSet(ds2.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActionIds() .get(0); - final Long actionId3 = assignDistributionSet(ds3.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActions() + final Long actionId3 = assignDistributionSet(ds3.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActionIds() .get(0); assertThat(deploymentManagement.countActionStatusAll()).isEqualTo(3); @@ -452,7 +451,7 @@ public void tooMuchCancelActionFeedback() throws Exception { testdataFactory.createTarget(); final DistributionSet ds = testdataFactory.createDistributionSet(""); - final Long actionId = assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActions() + final Long actionId = assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActionIds() .get(0); final Action cancelAction = deploymentManagement.cancelAction(actionId); 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 5a342edc20..1fa4aade9d 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 @@ -267,7 +267,7 @@ public void changeEtagIfActionSwitchesFromSoftToForced() throws Exception { final DistributionSet ds = testdataFactory.createDistributionSet("", true); final Long actionId = deploymentManagement.assignDistributionSet(ds.getId(), ActionType.TIMEFORCED, - System.currentTimeMillis() + 2_000, Arrays.asList(target.getControllerId())).getActions().get(0); + System.currentTimeMillis() + 2_000, Arrays.asList(target.getControllerId())).getActionIds().get(0); MvcResult mvcResult = mvc.perform(get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant())) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) @@ -566,7 +566,7 @@ public void badDeploymentAction() throws Exception { final List toAssign = Arrays.asList(target); final DistributionSet savedSet = testdataFactory.createDistributionSet(""); - final Long actionId = assignDistributionSet(savedSet, toAssign).getActions().get(0); + final Long actionId = assignDistributionSet(savedSet, toAssign).getActionIds().get(0); mvc.perform(get("/{tenant}/controller/v1/4712/deploymentBase/" + actionId, tenantAware.getCurrentTenant())) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); mvc.perform(get("/{tenant}/controller/v1/4712/deploymentBase/" + actionId, tenantAware.getCurrentTenant()) @@ -639,9 +639,9 @@ public void multipleDeplomentActionFeedback() throws Exception { final List toAssign = new ArrayList<>(); toAssign.add(savedTarget1); - final Long actionId1 = assignDistributionSet(ds1.getId(), "4712").getActions().get(0); - final Long actionId2 = assignDistributionSet(ds2.getId(), "4712").getActions().get(0); - final Long actionId3 = assignDistributionSet(ds3.getId(), "4712").getActions().get(0); + final Long actionId1 = assignDistributionSet(ds1.getId(), "4712").getActionIds().get(0); + final Long actionId2 = assignDistributionSet(ds2.getId(), "4712").getActionIds().get(0); + final Long actionId3 = assignDistributionSet(ds3.getId(), "4712").getActionIds().get(0); Target myT = targetManagement.getByControllerID("4712").get(); assertThat(myT.getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING); diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java index 2a431802a6..90a307f627 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java @@ -29,7 +29,9 @@ import java.util.Collections; import java.util.Map; +import java.util.UUID; +import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants; import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; @@ -258,9 +260,8 @@ public void rootRsNotModified() throws Exception { etagWithFirstUpdate)).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotModified()); // now lets finish the update - sendDeploymentActionFeedback(target, updateAction, - JsonBuilder.deploymentActionFeedback(updateAction.getId().toString(), "closed")) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + sendDeploymentActionFeedback(target, updateAction, "closed", null).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); // we are again at the original state mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()).header("If-None-Match", etag)) @@ -364,7 +365,6 @@ public void rootRsIpAddressNotStoredIfDisabled() throws Exception { @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1), @Expect(type = TargetUpdatedEvent.class, count = 2), - @Expect(type = TargetAttributesRequestedEvent.class, count = 1), @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) public void tryToFinishAnUpdateProcessAfterItHasBeenFinished() throws Exception { final DistributionSet ds = testdataFactory.createDistributionSet(""); @@ -373,17 +373,14 @@ public void tryToFinishAnUpdateProcessAfterItHasBeenFinished() throws Exception .next(); final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) .getContent().get(0); - sendDeploymentActionFeedback(savedTarget, savedAction, - JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "proceeding")) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + sendDeploymentActionFeedback(savedTarget, savedAction, "proceeding", null).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, - JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "closed", "failure")) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "failure").andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, - JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "closed", "success")) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isGone()); + sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success").andDo(MockMvcResultPrinter.print()) + .andExpect(status().isGone()); } @Test @@ -419,9 +416,7 @@ private void assertAttributesUpdateNotRequestedAfterFailedDeployment(Target targ target = assignDistributionSet(ds.getId(), target.getControllerId()).getAssignedEntity().iterator().next(); final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId()) .getContent().get(0); - sendDeploymentActionFeedback(target, action, - JsonBuilder.deploymentActionFeedback(action.getId().toString(), "closed", "failure", "")) - .andExpect(status().isOk()); + sendDeploymentActionFeedback(target, action, "closed", "failure").andExpect(status().isOk()); assertThatAttributesUpdateIsNotRequested(target.getControllerId()); } @@ -431,8 +426,7 @@ private void assertAttributesUpdateRequestedAfterSuccessfulDeployment(Target tar target = assignDistributionSet(ds.getId(), target.getControllerId()).getAssignedEntity().iterator().next(); final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId()) .getContent().get(0); - sendDeploymentActionFeedback(target, action, - JsonBuilder.deploymentActionFeedback(action.getId().toString(), "closed")).andExpect(status().isOk()); + sendDeploymentActionFeedback(target, action, "closed", null).andExpect(status().isOk()); assertThatAttributesUpdateIsRequested(target.getControllerId()); } @@ -448,13 +442,26 @@ private void assertThatAttributesUpdateIsNotRequested(final String targetControl .andExpect(jsonPath("$._links.configData").doesNotExist()); } - private ResultActions sendDeploymentActionFeedback(final Target target, final Action action, final String feedback) - throws Exception { + private ResultActions sendDeploymentActionFeedback(final Target target, final Action action, final String execution, + String finished, String message) throws Exception { + if (finished == null) { + finished = "none"; + } + if (message == null) { + message = RandomStringUtils.randomAlphanumeric(1000); + } + final String feedback = JsonBuilder.deploymentActionFeedback(action.getId().toString(), execution, finished, + message); return mvc.perform(post("/{tenant}/controller/v1/{controllerId}/deploymentBase/{actionId}/feedback", tenantAware.getCurrentTenant(), target.getControllerId(), action.getId()).content(feedback) .contentType(MediaType.APPLICATION_JSON)); } + private ResultActions sendDeploymentActionFeedback(final Target target, final Action action, final String execution, + final String finished) throws Exception { + return sendDeploymentActionFeedback(target, action, execution, finished, null); + } + @Test @Description("Test to verify that only a specific count of messages are returned based on the input actionHistory for getControllerDeploymentActionFeedback endpoint.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @@ -472,20 +479,14 @@ public void testActionHistoryCount() throws Exception { final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) .getContent().get(0); - sendDeploymentActionFeedback(savedTarget, savedAction, - JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "scheduled", - TARGET_SCHEDULED_INSTALLATION_MSG)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isOk()); + sendDeploymentActionFeedback(savedTarget, savedAction, "scheduled", null, TARGET_SCHEDULED_INSTALLATION_MSG) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, - JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "proceeding", - TARGET_PROCEEDING_INSTALLATION_MSG)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isOk()); + sendDeploymentActionFeedback(savedTarget, savedAction, "proceeding", null, TARGET_PROCEEDING_INSTALLATION_MSG) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, - JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "closed", "success", - TARGET_COMPLETED_INSTALLATION_MSG)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isOk()); + sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success", TARGET_COMPLETED_INSTALLATION_MSG) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); mvc.perform(get("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "?actionHistory=3", tenantAware.getCurrentTenant()).contentType(MediaType.APPLICATION_JSON) @@ -514,20 +515,14 @@ public void testActionHistoryZeroInput() throws Exception { final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) .getContent().get(0); - sendDeploymentActionFeedback(savedTarget, savedAction, - JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "scheduled", - TARGET_SCHEDULED_INSTALLATION_MSG)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isOk()); + sendDeploymentActionFeedback(savedTarget, savedAction, "scheduled", null, TARGET_SCHEDULED_INSTALLATION_MSG) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, - JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "proceeding", - TARGET_PROCEEDING_INSTALLATION_MSG)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isOk()); + sendDeploymentActionFeedback(savedTarget, savedAction, "proceeding", null, TARGET_PROCEEDING_INSTALLATION_MSG) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, - JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "closed", "success", - TARGET_COMPLETED_INSTALLATION_MSG)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isOk()); + sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success", TARGET_COMPLETED_INSTALLATION_MSG) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); mvc.perform(get("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "?actionHistory=-2", tenantAware.getCurrentTenant()).contentType(MediaType.APPLICATION_JSON) @@ -552,20 +547,14 @@ public void testActionHistoryNegativeInput() throws Exception { final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) .getContent().get(0); - sendDeploymentActionFeedback(savedTarget, savedAction, - JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "scheduled", - TARGET_SCHEDULED_INSTALLATION_MSG)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isOk()); + sendDeploymentActionFeedback(savedTarget, savedAction, "scheduled", null, TARGET_SCHEDULED_INSTALLATION_MSG) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, - JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "proceeding", - TARGET_PROCEEDING_INSTALLATION_MSG)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isOk()); + sendDeploymentActionFeedback(savedTarget, savedAction, "proceeding", null, TARGET_PROCEEDING_INSTALLATION_MSG) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, - JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "closed", "success", - TARGET_COMPLETED_INSTALLATION_MSG)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isOk()); + sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success", TARGET_COMPLETED_INSTALLATION_MSG) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); mvc.perform(get("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "?actionHistory=-1", tenantAware.getCurrentTenant()).contentType(MediaType.APPLICATION_JSON) @@ -665,4 +654,34 @@ public void downloadAndUpdateStatusDuringMaintenanceWindow() throws Exception { .andExpect(jsonPath("$.deployment.update", equalTo("forced"))) .andExpect(jsonPath("$.deployment.maintenanceWindow", equalTo("available"))); } + + @Test + @Description("Assign multiple DS in multi-assignment mode. The earliest active Action is exposed to the controller.") + public void earliestActionIsExposedToControllerInMultiAssignMode() throws Exception { + setMultiAssignmentsEnabled(); + final Target target = testdataFactory.createTarget(); + final DistributionSet ds1 = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final DistributionSet ds2 = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + final Action action1 = assignDistributionSet(ds1, target).getActions().get(0); + final Action action2 = assignDistributionSet(ds2, target).getActions().get(0); + + assertDeploymentActionIsExposedToTarget(target.getControllerId(), action1.getId()); + sendDeploymentActionFeedback(target, action1, "closed", "success"); + assertDeploymentActionIsExposedToTarget(target.getControllerId(), action2.getId()); + + } + + public void assertDeploymentActionIsExposedToTarget(final String controllerId, final long expectedActionId) + throws Exception { + final String expectedDeploymentBaseLink = String.format("/%s/controller/v1/%s/deploymentBase/%d", + tenantAware.getCurrentTenant(), controllerId, expectedActionId); + mvc.perform(get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(), controllerId) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$._links.deploymentBase.href", containsString(expectedDeploymentBaseLink))); + + } + + private void setMultiAssignmentsEnabled() { + tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, true); + } } 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 d8d2ce1874..6419373e05 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 @@ -1239,7 +1239,7 @@ public void updateAction() throws Exception { final DistributionSet set = testdataFactory.createDistributionSet(); final Long actionId = deploymentManagement .assignDistributionSet(set.getId(), ActionType.SOFT, 0, Arrays.asList(target.getControllerId())) - .getActions().get(0); + .getActionIds().get(0); assertThat(deploymentManagement.findAction(actionId).get().getActionType()).isEqualTo(ActionType.SOFT); final String body = new JSONObject().put("forceType", "forced").toString(); diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java new file mode 100644 index 0000000000..ac16e74e76 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java @@ -0,0 +1,73 @@ +/** + * 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 static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; +import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; +import org.json.JSONObject; +import org.junit.Test; +import org.springframework.http.MediaType; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; + +/** + * Spring MVC Tests against the MgmtTenantManagementResource. + * + */ +@Feature("Component Tests - Management API") +@Story("Tenant Management Resource") +public class MgmtTenantManagementResourceTest extends AbstractManagementApiIntegrationTest { + + private static final String KEY_MULTI_ASSIGNMENTS = "multi.assignments.enabled"; + + private static final String KEY_AUTO_CLOSE = "repository.actions.autoclose.enabled"; + + @Test + @Description("The 'multi.assignments.enabled' property must not be changed to false.") + public void deactivateMultiAssignment() throws Exception { + final String bodyActivate = new JSONObject().put("value", true).toString(); + final String bodyDeactivate = new JSONObject().put("value", false).toString(); + + mvc.perform(put(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs/{keyName}", KEY_MULTI_ASSIGNMENTS) + .content(bodyActivate).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + mvc.perform(put(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs/{keyName}", KEY_MULTI_ASSIGNMENTS) + .content(bodyDeactivate).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isForbidden()); + } + + @Test + @Description("The 'repository.actions.autoclose.enabled' property must not be modified if Multi-Assignments is enabled.") + public void autoCloseCannotBeModifiedIfMultiAssignmentIsEnabled() throws Exception { + final String bodyActivate = new JSONObject().put("value", true).toString(); + final String bodyDeactivate = new JSONObject().put("value", false).toString(); + + // enable Multi-Assignments + mvc.perform(put(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs/{keyName}", KEY_MULTI_ASSIGNMENTS) + .content(bodyActivate).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + // try to enable Auto-Close + mvc.perform(put(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs/{keyName}", KEY_AUTO_CLOSE) + .content(bodyActivate).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isForbidden()); + + // try to disable Auto-Close + mvc.perform(put(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs/{keyName}", KEY_AUTO_CLOSE) + .content(bodyDeactivate).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isForbidden()); + } + +} 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 856e3a39eb..a72e809202 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 @@ -78,6 +78,7 @@ public class ResponseExceptionHandler { ERROR_TO_HTTP_STATUS.put(SpServerError.SP_TARGET_ATTRIBUTES_INVALID, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_AUTO_ASSIGN_ACTION_TYPE_INVALID, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_AUTO_ASSIGN_DISTRIBUTION_SET_INVALID, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED, HttpStatus.FORBIDDEN); } private static HttpStatus getStatusOrDefault(final SpServerError error) { 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 4d2bb24fe5..c0907ff17c 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 @@ -138,7 +138,7 @@ public void getControllerCancelAction() throws Exception { final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); final Long actionId = deploymentManagement - .assignDistributionSet(set.getId(), Arrays.asList(target.getTargetWithActionType())).getActions() + .assignDistributionSet(set.getId(), Arrays.asList(target.getTargetWithActionType())).getActionIds() .get(0); final Action cancelAction = deploymentManagement.cancelAction(actionId); @@ -171,7 +171,7 @@ public void postCancelActionFeedback() throws Exception { final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); final Long actionId = deploymentManagement - .assignDistributionSet(set.getId(), Arrays.asList(target.getTargetWithActionType())).getActions() + .assignDistributionSet(set.getId(), Arrays.asList(target.getTargetWithActionType())).getActionIds() .get(0); final Action cancelAction = deploymentManagement.cancelAction(actionId); @@ -265,7 +265,7 @@ public void getControllerBasedeploymentAction() throws Exception { final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); final Long actionId = assignDistributionSetWithMaintenanceWindow(set.getId(), target.getControllerId(), - getTestSchedule(-5), getTestDuration(10), getTestTimeZone()).getActions().get(0); + getTestSchedule(-5), getTestDuration(10), getTestTimeZone()).getActionIds().get(0); controllerManagement.addInformationalActionStatus( entityFactory.actionStatus().create(actionId).message("Started download").status(Status.DOWNLOAD)); @@ -347,7 +347,7 @@ public void getControllerBasedeploymentActionWithMaintenanceWindow() throws Exce final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); final Long actionId = assignDistributionSetWithMaintenanceWindow(set.getId(), target.getControllerId(), - getTestSchedule(2), getTestDuration(1), getTestTimeZone()).getActions().get(0); + getTestSchedule(2), getTestDuration(1), getTestTimeZone()).getActionIds().get(0); mockMvc.perform(get( DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/" + DdiRestConstants.DEPLOYMENT_BASE_ACTION @@ -388,7 +388,7 @@ public void postBasedeploymentActionFeedback() throws Exception { final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); final Long actionId = deploymentManagement - .assignDistributionSet(set.getId(), Arrays.asList(target.getTargetWithActionType())).getActions() + .assignDistributionSet(set.getId(), Arrays.asList(target.getTargetWithActionType())).getActionIds() .get(0); mockMvc.perform(post(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/" 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 64236952b5..35da1ce0fc 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 @@ -397,7 +397,7 @@ public void switchActionToForced() throws Exception { final DistributionSet set = testdataFactory.createDistributionSet(); final Long actionId = deploymentManagement .assignDistributionSet(set.getId(), ActionType.SOFT, 0, Arrays.asList(target.getControllerId())) - .getActions().get(0); + .getActionIds().get(0); assertThat(deploymentManagement.findAction(actionId).get().getActionType()).isEqualTo(ActionType.SOFT); final Map body = new HashMap<>(); diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java index 2194ddd4c7..603d096609 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java @@ -88,6 +88,8 @@ public class TenantResourceDocumentationTest extends AbstractApiRestDocumentatio "the list of action status that should be taken into account for the cleanup."); CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.ACTION_CLEANUP_ACTION_EXPIRY, "the expiry time in milliseconds that needs to elapse before an action may be cleaned up."); + CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, + "if multiple distribution sets can be assigned to the same targets."); } @Autowired diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/UiProperties.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/UiProperties.java index cb1c11dec4..90a0de743c 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/UiProperties.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/UiProperties.java @@ -209,6 +209,8 @@ public static class Documentation implements Serializable { */ private String rolloutView = ""; + private String provisioningStateMachine = ""; + public String getDeploymentView() { return deploymentView; } @@ -249,6 +251,10 @@ public String getMaintenanceWindowView() { return maintenanceWindowView; } + public String getProvisioningStateMachine() { + return provisioningStateMachine; + } + public void setDeploymentView(final String deploymentView) { this.deploymentView = deploymentView; } @@ -289,6 +295,10 @@ public void setMaintenanceWindowView(final String maintenanceWindowView) { this.maintenanceWindowView = maintenanceWindowView; } + public void setProvisioningStateMachine(final String provisioningStateMachine) { + this.provisioningStateMachine = provisioningStateMachine; + } + } private final Documentation documentation = new Documentation(); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/AbstractDistributionSetDetails.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/AbstractDistributionSetDetails.java index d741641503..2dee9c11d2 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/AbstractDistributionSetDetails.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/AbstractDistributionSetDetails.java @@ -12,7 +12,9 @@ import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetTagManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.common.tagdetails.DistributionTagToken; import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; @@ -56,6 +58,8 @@ public abstract class AbstractDistributionSetDetails private final SoftwareModuleDetailsTable softwareModuleDetailsTable; + private final transient TenantConfigurationManagement tenantConfigurationManagement; + private VerticalLayout softwareModuleTab; protected AbstractDistributionSetDetails(final VaadinMessageSource i18n, final UIEventBus eventBus, @@ -64,7 +68,8 @@ protected AbstractDistributionSetDetails(final VaadinMessageSource i18n, final U final DistributionSetManagement distributionSetManagement, final DsMetadataPopupLayout dsMetadataPopupLayout, final UINotification uiNotification, final DistributionSetTagManagement distributionSetTagManagement, - final SoftwareModuleDetailsTable softwareModuleDetailsTable) { + final SoftwareModuleDetailsTable softwareModuleDetailsTable, + final TenantConfigurationManagement tenantConfigurationManagement) { super(i18n, eventBus, permissionChecker, managementUIState); this.distributionAddUpdateWindowLayout = distributionAddUpdateWindowLayout; this.uiNotification = uiNotification; @@ -73,6 +78,7 @@ protected AbstractDistributionSetDetails(final VaadinMessageSource i18n, final U this.distributionTagToken = new DistributionTagToken(permissionChecker, i18n, uiNotification, eventBus, managementUIState, distributionSetTagManagement, distributionSetManagement); this.softwareModuleDetailsTable = softwareModuleDetailsTable; + this.tenantConfigurationManagement = tenantConfigurationManagement; dsMetadataTable = new DistributionSetMetadataDetailsLayout(i18n, distributionSetManagement, dsMetadataPopupLayout); @@ -175,9 +181,19 @@ private void updateDistributionSetDetailsLayout(final String type, final Boolean typeLabel.setId(UIComponentIdProvider.DETAILS_TYPE_LABEL_ID); detailsTabLayout.addComponent(typeLabel); - detailsTabLayout.addComponent( - SPUIComponentProvider.createNameValueLabel(getI18n().getMessage("checkbox.dist.migration.required"), - getMigrationRequiredValue(isMigrationRequired))); + final Label requiredMigrationStepLabel = SPUIComponentProvider.createNameValueLabel( + getI18n().getMessage("checkbox.dist.migration.required"), + getMigrationRequiredValue(isMigrationRequired)); + requiredMigrationStepLabel.setId(UIComponentIdProvider.DETAILS_REQUIRED_MIGRATION_STEP_LABEL_ID); + if (!isMultiAssignmentEnabled()) { + detailsTabLayout.addComponent(requiredMigrationStepLabel); + } + + } + + private boolean isMultiAssignmentEnabled() { + return tenantConfigurationManagement + .getConfigurationValue(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, Boolean.class).getValue(); } private String getMigrationRequiredValue(final Boolean isMigrationRequired) { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/DistributionsView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/DistributionsView.java index 4421560310..45509c96e4 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/DistributionsView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/DistributionsView.java @@ -21,6 +21,7 @@ import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.ui.AbstractHawkbitUI; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent; @@ -102,7 +103,8 @@ public class DistributionsView extends AbstractNotificationView implements Brows final DistributionsViewClientCriterion distributionsViewClientCriterion, final ArtifactUploadState artifactUploadState, final SystemManagement systemManagement, final ArtifactManagement artifactManagement, final NotificationUnreadButton notificationUnreadButton, - final DistributionsViewMenuItem distributionsViewMenuItem) { + final DistributionsViewMenuItem distributionsViewMenuItem, + final TenantConfigurationManagement configManagement) { super(eventBus, notificationUnreadButton); this.permChecker = permChecker; this.i18n = i18n; @@ -117,7 +119,7 @@ public class DistributionsView extends AbstractNotificationView implements Brows this.distributionTableLayout = new DistributionSetTableLayout(i18n, eventBus, permChecker, manageDistUIState, softwareModuleManagement, distributionSetManagement, distributionSetTypeManagement, targetManagement, entityFactory, uiNotification, distributionSetTagManagement, distributionsViewClientCriterion, - systemManagement); + systemManagement, configManagement); this.softwareModuleTableLayout = new SwModuleTableLayout(i18n, uiNotification, eventBus, softwareModuleManagement, softwareModuleTypeManagement, entityFactory, manageDistUIState, permChecker, distributionsViewClientCriterion, artifactUploadState, artifactManagement); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DistributionSetDetails.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DistributionSetDetails.java index 625d48fde7..ff5427d2a6 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DistributionSetDetails.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DistributionSetDetails.java @@ -13,6 +13,7 @@ import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetTagManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent; @@ -58,11 +59,12 @@ public class DistributionSetDetails extends AbstractDistributionSetDetails { final DistributionAddUpdateWindowLayout distributionAddUpdateWindowLayout, final DistributionSetManagement distributionSetManagement, final UINotification uiNotification, final DistributionSetTagManagement distributionSetTagManagement, - final DsMetadataPopupLayout dsMetadataPopupLayout) { + final DsMetadataPopupLayout dsMetadataPopupLayout, final TenantConfigurationManagement configManagement) { super(i18n, eventBus, permissionChecker, managementUIState, distributionAddUpdateWindowLayout, distributionSetManagement, dsMetadataPopupLayout, uiNotification, distributionSetTagManagement, createSoftwareModuleDetailsTable(i18n, permissionChecker, distributionSetManagement, eventBus, - manageDistUIState, uiNotification)); + manageDistUIState, uiNotification), + configManagement); this.manageDistUIState = manageDistUIState; tfqDetailsTable = new TargetFilterQueryDetailsTable(i18n); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DistributionSetTableLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DistributionSetTableLayout.java index 3f18493114..e6fa68df1a 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DistributionSetTableLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DistributionSetTableLayout.java @@ -15,6 +15,7 @@ import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.common.table.AbstractTableLayout; import org.eclipse.hawkbit.ui.dd.criteria.DistributionsViewClientCriterion; @@ -41,7 +42,7 @@ public DistributionSetTableLayout(final VaadinMessageSource i18n, final UIEventB final EntityFactory entityFactory, final UINotification uiNotification, final DistributionSetTagManagement distributionSetTagManagement, final DistributionsViewClientCriterion distributionsViewClientCriterion, - final SystemManagement systemManagement) { + final SystemManagement systemManagement, final TenantConfigurationManagement configManagement) { this.distributionSetTable = new DistributionSetTable(eventBus, i18n, uiNotification, permissionChecker, manageDistUIState, distributionSetManagement, softwareManagement, distributionsViewClientCriterion, @@ -49,7 +50,7 @@ public DistributionSetTableLayout(final VaadinMessageSource i18n, final UIEventB final DistributionAddUpdateWindowLayout distributionAddUpdateWindowLayout = new DistributionAddUpdateWindowLayout( i18n, uiNotification, eventBus, distributionSetManagement, distributionSetTypeManagement, - systemManagement, entityFactory, distributionSetTable); + systemManagement, entityFactory, distributionSetTable, configManagement); final DsMetadataPopupLayout popupLayout = new DsMetadataPopupLayout(i18n, uiNotification, eventBus, distributionSetManagement, entityFactory, permissionChecker); @@ -60,7 +61,7 @@ public DistributionSetTableLayout(final VaadinMessageSource i18n, final UIEventB distributionSetTable, new DistributionSetDetails(i18n, eventBus, permissionChecker, manageDistUIState, null, distributionAddUpdateWindowLayout, distributionSetManagement, uiNotification, - distributionSetTagManagement, popupLayout)); + distributionSetTagManagement, popupLayout, configManagement)); } public DistributionSetTable getDistributionSetTable() { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/DeploymentView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/DeploymentView.java index 97f97df534..7ac5641616 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/DeploymentView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/DeploymentView.java @@ -22,6 +22,7 @@ import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetTagManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.ui.AbstractHawkbitUI; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.UiProperties; @@ -127,6 +128,7 @@ public class DeploymentView extends AbstractNotificationView implements BrowserW final TargetTagManagement targetTagManagement, final DistributionSetTagManagement distributionSetTagManagement, final TargetFilterQueryManagement targetFilterQueryManagement, final SystemManagement systemManagement, + final TenantConfigurationManagement configManagement, final NotificationUnreadButton notificationUnreadButton, final DeploymentViewMenuItem deploymentViewMenuItem, @Qualifier("uiExecutor") final Executor uiExecutor) { super(eventBus, notificationUnreadButton); @@ -147,7 +149,7 @@ public class DeploymentView extends AbstractNotificationView implements BrowserW targetFilterQueryManagement, targetTagManagement); final TargetTable targetTable = new TargetTable(eventBus, i18n, uiNotification, targetManagement, managementUIState, permChecker, managementViewClientCriterion, distributionSetManagement, - targetTagManagement, deploymentManagement, uiProperties); + targetTagManagement, deploymentManagement, configManagement, uiProperties); this.countMessageLabel = new CountMessageLabel(eventBus, targetManagement, i18n, managementUIState, targetTable); @@ -175,7 +177,7 @@ public class DeploymentView extends AbstractNotificationView implements BrowserW this.distributionTableLayout = new DistributionTableLayout(i18n, eventBus, permChecker, managementUIState, distributionSetManagement, distributionSetTypeManagement, managementViewClientCriterion, entityFactory, uiNotification, distributionSetTagManagement, targetTagManagement, systemManagement, - targetManagement, deploymentManagement, uiProperties); + targetManagement, deploymentManagement, configManagement, uiProperties); } else { this.distributionTagLayout = null; this.distributionTableLayout = null; 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 b77f739ce7..2b45ff6e30 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,9 +8,7 @@ */ package org.eclipse.hawkbit.ui.management; -import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -20,17 +18,19 @@ import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException; import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; +import org.eclipse.hawkbit.repository.model.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.DistributionSetIdName; import org.eclipse.hawkbit.ui.common.entity.TargetIdName; import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; import org.eclipse.hawkbit.ui.management.event.PinUnpinEvent; import org.eclipse.hawkbit.ui.management.event.SaveActionWindowEvent; import org.eclipse.hawkbit.ui.management.miscs.AbstractActionTypeOptionGroupLayout; +import org.eclipse.hawkbit.ui.management.miscs.AbstractActionTypeOptionGroupLayout.ActionTypeOption; import org.eclipse.hawkbit.ui.management.miscs.ActionTypeOptionGroupAssignmentLayout; import org.eclipse.hawkbit.ui.management.miscs.MaintenanceWindowLayout; import org.eclipse.hawkbit.ui.management.state.ManagementUIState; @@ -41,7 +41,6 @@ import org.slf4j.LoggerFactory; import org.vaadin.spring.events.EventBus.UIEventBus; -import com.google.common.collect.Maps; import com.vaadin.data.Property; import com.vaadin.ui.CheckBox; import com.vaadin.ui.HorizontalLayout; @@ -59,8 +58,12 @@ private TargetAssignmentOperations() { } /** - * Save all target(s)-distributionSet assignments + * Save the given distribution set assignments * + * @param targets + * to assign the given distribution sets to + * @param distributionSets + * to assign to the given targets * @param managementUIState * the management UI state * @param actionTypeOptionGroupLayout @@ -78,79 +81,64 @@ private TargetAssignmentOperations() { * @param eventSource * the source object for sending potential events */ - public static void saveAllAssignments(final ManagementUIState managementUIState, + public static void saveAllAssignments(final List targets, final List distributionSets, + final ManagementUIState managementUIState, final ActionTypeOptionGroupAssignmentLayout actionTypeOptionGroupLayout, final MaintenanceWindowLayout maintenanceWindowLayout, final DeploymentManagement deploymentManagement, final UINotification notification, final UIEventBus eventBus, final VaadinMessageSource i18n, final Object eventSource) { - final Set itemIds = managementUIState.getAssignedList().keySet(); - Long distId; - List targetIdSetList; - List tempIdList; - final ActionType actionType = ((AbstractActionTypeOptionGroupLayout.ActionTypeOption) actionTypeOptionGroupLayout - .getActionTypeOptionGroup().getValue()).getActionType(); - final long forcedTimeStamp = actionTypeOptionGroupLayout.getActionTypeOptionGroup() - .getValue() == AbstractActionTypeOptionGroupLayout.ActionTypeOption.AUTO_FORCED - ? actionTypeOptionGroupLayout.getForcedTimeDateField().getValue().getTime() - : RepositoryModelConstants.NO_FORCE_TIME; - final Map> saveAssignedList = Maps.newHashMapWithExpectedSize(itemIds.size()); + final ActionType actionType = ((ActionTypeOption) actionTypeOptionGroupLayout.getActionTypeOptionGroup() + .getValue()).getActionType(); - for (final TargetIdName itemId : itemIds) { - final DistributionSetIdName distitem = managementUIState.getAssignedList().get(itemId); - distId = distitem.getId(); - - if (saveAssignedList.containsKey(distId)) { - targetIdSetList = saveAssignedList.get(distId); - } else { - targetIdSetList = new ArrayList<>(); - } - targetIdSetList.add(itemId); - saveAssignedList.put(distId, targetIdSetList); - } + final long forcedTimeStamp = (((ActionTypeOption) actionTypeOptionGroupLayout.getActionTypeOptionGroup() + .getValue()) == ActionTypeOption.AUTO_FORCED) + ? actionTypeOptionGroupLayout.getForcedTimeDateField().getValue().getTime() + : RepositoryModelConstants.NO_FORCE_TIME; final String maintenanceSchedule = maintenanceWindowLayout.getMaintenanceSchedule(); final String maintenanceDuration = maintenanceWindowLayout.getMaintenanceDuration(); final String maintenanceTimeZone = maintenanceWindowLayout.getMaintenanceTimeZone(); - for (final Map.Entry> mapEntry : saveAssignedList.entrySet()) { - tempIdList = saveAssignedList.get(mapEntry.getKey()); - final DistributionSetAssignmentResult distributionSetAssignmentResult = deploymentManagement - .assignDistributionSet(mapEntry.getKey(), - tempIdList.stream().map(t -> maintenanceWindowLayout.isEnabled() - ? new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp, - maintenanceSchedule, maintenanceDuration, maintenanceTimeZone) - : new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp)) - .collect(Collectors.toList())); - if (distributionSetAssignmentResult.getAssigned() > 0) { - notification.displaySuccess( - i18n.getMessage("message.target.assignment", distributionSetAssignmentResult.getAssigned())); - } - if (distributionSetAssignmentResult.getAlreadyAssigned() > 0) { - notification.displaySuccess(i18n.getMessage("message.target.alreadyAssigned", - distributionSetAssignmentResult.getAlreadyAssigned())); - } + 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 results = deploymentManagement.assignDistributionSets(dsIds, + trgActionType); + + // 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())); } - refreshPinnedDetails(saveAssignedList, managementUIState, eventBus, eventSource); - managementUIState.getAssignedList().clear(); + + 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 Map> saveAssignedList, + private static void refreshPinnedDetails(final Set dsIds, final Set targetIds, final ManagementUIState managementUIState, final UIEventBus eventBus, final Object eventSource) { final Optional pinnedDist = managementUIState.getTargetTableFilters().getPinnedDistId(); final Optional pinnedTarget = managementUIState.getDistributionTableFilters().getPinnedTarget(); if (pinnedDist.isPresent()) { - if (saveAssignedList.keySet().contains(pinnedDist.get())) { + if (dsIds.contains(pinnedDist.get())) { eventBus.publish(eventSource, PinUnpinEvent.PIN_DISTRIBUTION); } - } else if (pinnedTarget.isPresent()) { - final Set assignedTargetIds = managementUIState.getAssignedList().keySet(); - if (assignedTargetIds.contains(pinnedTarget.get())) { - eventBus.publish(eventSource, PinUnpinEvent.PIN_TARGET); - } + } else if (pinnedTarget.isPresent() && targetIds.contains(pinnedTarget.get().getTargetId())) { + eventBus.publish(eventSource, PinUnpinEvent.PIN_TARGET); } } @@ -187,7 +175,8 @@ public static boolean isMaintenanceWindowValid(final MaintenanceWindowLayout mai * @param maintenanceWindowLayout * the Maintenance Window Layout * @param saveButtonToggle - * The event listener to derimne if save button should be enabled or not + * The event listener to derimne if save button should be enabled + * or not * @param i18n * the Vaadin Message Source for multi language * @param uiProperties @@ -283,4 +272,4 @@ private static Link maintenanceWindowHelpLinkControl(final UiProperties uiProper return SPUIComponentProvider.getHelpLink(i18n, maintenanceWindowHelpUrl); } -} +} \ No newline at end of file diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionAddUpdateWindowLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionAddUpdateWindowLayout.java index 04cc16160f..d76e85f99b 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionAddUpdateWindowLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionAddUpdateWindowLayout.java @@ -15,10 +15,12 @@ import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.TenantMetaData; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.eclipse.hawkbit.ui.common.CommonDialogWindow; import org.eclipse.hawkbit.ui.common.CommonDialogWindow.SaveDialogCloseListener; import org.eclipse.hawkbit.ui.common.DistributionSetTypeBeanQuery; @@ -64,6 +66,7 @@ public class DistributionAddUpdateWindowLayout extends CustomComponent { private final transient DistributionSetTypeManagement distributionSetTypeManagement; private final transient SystemManagement systemManagement; private final transient EntityFactory entityFactory; + private final transient TenantConfigurationManagement tenantConfigurationManagement; private final DistributionSetTable distributionSetTable; @@ -94,11 +97,14 @@ public class DistributionAddUpdateWindowLayout extends CustomComponent { * EntityFactory * @param distributionSetTable * DistributionSetTable + * @param tenantConfigurationManagement + * TenantConfigurationManagement */ public DistributionAddUpdateWindowLayout(final VaadinMessageSource i18n, final UINotification notificationMessage, final UIEventBus eventBus, final DistributionSetManagement distributionSetManagement, final DistributionSetTypeManagement distributionSetTypeManagement, final SystemManagement systemManagement, - final EntityFactory entityFactory, final DistributionSetTable distributionSetTable) { + final EntityFactory entityFactory, final DistributionSetTable distributionSetTable, + final TenantConfigurationManagement tenantConfigurationManagement) { this.i18n = i18n; this.notificationMessage = notificationMessage; this.eventBus = eventBus; @@ -107,6 +113,7 @@ public DistributionAddUpdateWindowLayout(final VaadinMessageSource i18n, final U this.systemManagement = systemManagement; this.entityFactory = entityFactory; this.distributionSetTable = distributionSetTable; + this.tenantConfigurationManagement = tenantConfigurationManagement; createRequiredComponents(); buildLayout(); } @@ -134,8 +141,8 @@ public void saveOrUpdate() { final DistributionSet currentDS = distributionSetManagement.update(entityFactory.distributionSet() .update(editDistId).name(distNameTextField.getValue()).description(descTextArea.getValue()) .version(distVersionTextField.getValue()).requiredMigrationStep(isMigStepReq)); - notificationMessage.displaySuccess(i18n.getMessage("message.new.dist.save.success", - new Object[] { currentDS.getName(), currentDS.getVersion() })); + notificationMessage.displaySuccess( + i18n.getMessage("message.new.dist.save.success", currentDS.getName(), currentDS.getVersion())); // update table row+details layout eventBus.publish(this, new DistributionTableEvent(BaseEntityEventType.UPDATED_ENTITY, currentDS)); }); @@ -283,6 +290,12 @@ public void resetComponents() { distsetTypeNameComboBox.setEnabled(true); descTextArea.clear(); reqMigStepCheckbox.clear(); + reqMigStepCheckbox.setVisible(!isMultiAssignmentEnabled()); + } + + private boolean isMultiAssignmentEnabled() { + return tenantConfigurationManagement.getConfigurationValue(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, + Boolean.class).getValue(); } private void populateValuesOfDistribution(final Long editDistId) { @@ -351,7 +364,8 @@ private CommonDialogWindow getWindow(final Long editDistId) { populateValuesOfDistribution(editDistId); } - return new WindowBuilder(SPUIDefinitions.CREATE_UPDATE_WINDOW).caption(caption).content(this).layout(formLayout) + return new WindowBuilder(SPUIDefinitions.CREATE_UPDATE_WINDOW).caption(caption).content(this) + .id(UIComponentIdProvider.CREATE_POPUP_ID).layout(formLayout) .i18n(i18n).saveDialogCloseListener(saveDialogCloseListener).buildCommonDialogWindow(); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionDetails.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionDetails.java index 050eb94d29..a745b34d96 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionDetails.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionDetails.java @@ -10,6 +10,7 @@ import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetTagManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.common.detailslayout.AbstractDistributionSetDetails; import org.eclipse.hawkbit.ui.common.detailslayout.SoftwareModuleDetailsTable; @@ -31,10 +32,12 @@ public class DistributionDetails extends AbstractDistributionSetDetails { final DistributionSetManagement distributionSetManagement, final DsMetadataPopupLayout dsMetadataPopupLayout, final UINotification uiNotification, final DistributionSetTagManagement distributionSetTagManagement, - final DistributionAddUpdateWindowLayout distributionAddUpdateWindowLayout) { + final DistributionAddUpdateWindowLayout distributionAddUpdateWindowLayout, + final TenantConfigurationManagement tenantConfigurationManagement) { super(i18n, eventBus, permissionChecker, managementUIState, distributionAddUpdateWindowLayout, distributionSetManagement, dsMetadataPopupLayout, uiNotification, distributionSetTagManagement, - createSoftwareModuleDetailsTable(i18n, permissionChecker, uiNotification)); + createSoftwareModuleDetailsTable(i18n, permissionChecker, uiNotification), + tenantConfigurationManagement); restoreState(); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTable.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTable.java index aa13acd436..83ab708a4b 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTable.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTable.java @@ -404,84 +404,61 @@ private void assignTargetToDs(final DragAndDropEvent event) { assignTargetToDs(getItem(distItemId), targetManagement.get(targetIdSet)); } - private void assignTargetToDs(final Item item, final List targetDetailsList) { + private void assignTargetToDs(final Item item, final List targets) { if (item == null || item.getItemProperty("id") == null) { return; } - if (targetDetailsList.isEmpty()) { + if (targets.isEmpty()) { getNotification().displayWarning(getI18n().getMessage(TARGETS_NOT_EXISTS)); return; } final Long distId = (Long) item.getItemProperty("id").getValue(); selectDroppedEntities(distId); - final Optional findDistributionSetById = distributionSetManagement.get(distId); - if (!findDistributionSetById.isPresent()) { + final Optional distributionSet = distributionSetManagement.get(distId); + if (!distributionSet.isPresent()) { getNotification().displayWarning(getI18n().getMessage(DISTRIBUTIONSET_NOT_EXISTS)); return; } - addNewDistributionToAssignmentList(targetDetailsList, findDistributionSetById.get()); - openConfirmationWindowForAssignment(findDistributionSetById.get().getName(), targetDetailsList); - } - - private void addNewDistributionToAssignmentList(final List targetDetailsList, - final DistributionSet distributionSet) { - String pendingActionMessage = null; - final DistributionSetIdName distributionSetIdName = new DistributionSetIdName(distributionSet); - - for (final Target target : targetDetailsList) { - final TargetIdName key = new TargetIdName(target); - if (managementUIState.getAssignedList().keySet().contains(key) - && managementUIState.getAssignedList().get(key).equals(distributionSetIdName)) { - pendingActionMessage = getPendingActionMessage(pendingActionMessage, target.getControllerId(), - HawkbitCommonUtil.getDistributionNameAndVersion(distributionSetIdName.getName(), - distributionSetIdName.getVersion())); - getNotification().displayValidationError(pendingActionMessage); - } else { - managementUIState.getAssignedList().put(key, distributionSetIdName); - } - } + openConfirmationWindowForAssignment(distributionSet.get(), targets); } - private void openConfirmationWindowForAssignment(final String distributionNameToAssign, - final List targetDetailsList) { - final String confirmQuestion = createConfirmationQuestionForAssignment(distributionNameToAssign, - targetDetailsList); - createConfirmationWindowForAssignment(confirmQuestion); + private void openConfirmationWindowForAssignment(final DistributionSet distributionSet, + final List targets) { + + final String question = getAssignmentConfirmationMessage(distributionSet, targets); + final String caption = getI18n().getMessage(CAPTION_ENTITY_ASSIGN_ACTION_CONFIRMBOX); + final String okLabel = getI18n().getMessage(UIMessageIdProvider.BUTTON_OK); + final String cancelLabel = getI18n().getMessage(UIMessageIdProvider.BUTTON_CANCEL); + + confirmDialog = new ConfirmationDialog(caption, question, okLabel, cancelLabel, ok -> { + if (ok && isMaintenanceWindowValid(maintenanceWindowLayout, getNotification())) { + saveAllAssignments(targets, Collections.singletonList(distributionSet), managementUIState, + actionTypeOptionGroupLayout, maintenanceWindowLayout, deploymentManagement, getNotification(), + getEventBus(), getI18n(), this); + } + }, createAssignmentTab(actionTypeOptionGroupLayout, maintenanceWindowLayout, saveButtonToggle(), getI18n(), + uiProperties), UIComponentIdProvider.DIST_SET_TO_TARGET_ASSIGNMENT_CONFIRM_ID); + UI.getCurrent().addWindow(confirmDialog.getWindow()); confirmDialog.getWindow().bringToFront(); } - private void createConfirmationWindowForAssignment(final String confirmQuestion) { - confirmDialog = new ConfirmationDialog(getI18n().getMessage(CAPTION_ENTITY_ASSIGN_ACTION_CONFIRMBOX), - confirmQuestion, getI18n().getMessage(UIMessageIdProvider.BUTTON_OK), - getI18n().getMessage(UIMessageIdProvider.BUTTON_CANCEL), ok -> { - if (ok && isMaintenanceWindowValid(maintenanceWindowLayout, getNotification())) { - saveAllAssignments(managementUIState, actionTypeOptionGroupLayout, maintenanceWindowLayout, - deploymentManagement, getNotification(), getEventBus(), getI18n(), this); - } else { - managementUIState.getAssignedList().clear(); - } - }, createAssignmentTab(actionTypeOptionGroupLayout, maintenanceWindowLayout, saveButtonToggle(), - getI18n(), uiProperties), - UIComponentIdProvider.DIST_SET_TO_TARGET_ASSIGNMENT_CONFIRM_ID); - } - private Consumer saveButtonToggle() { return isEnabled -> confirmDialog.getOkButton().setEnabled(isEnabled); } - private String createConfirmationQuestionForAssignment(final String distributionNameToAssign, - final List targetDetailsList) { - if (targetDetailsList.size() == 1) { - return getI18n().getMessage(MESSAGE_CONFIRM_ASSIGN_ENTITY, distributionNameToAssign, "target", - targetDetailsList.get(0).getName()); - } else { - return getI18n().getMessage(MESSAGE_CONFIRM_ASSIGN_MULTIPLE_ENTITIES, targetDetailsList.size(), "targets", - distributionNameToAssign); + private String getAssignmentConfirmationMessage(final DistributionSet distributionSet, final List targets) { + final String distributionName = distributionSet.getName(); + final int targetCount = targets.size(); + if (targetCount > 1) { + return getI18n().getMessage(MESSAGE_CONFIRM_ASSIGN_MULTIPLE_ENTITIES, targetCount, "targets", + distributionName); } + return getI18n().getMessage(MESSAGE_CONFIRM_ASSIGN_ENTITY, distributionName, "target", + targets.get(0).getName()); } @Override @@ -516,15 +493,6 @@ private boolean isNoTagButton(final String tagData, final String targetNoTagData return false; } - private String getPendingActionMessage(final String message, final String controllerId, - final String distNameVersion) { - String pendActionMsg = getI18n().getMessage("message.target.assigned.pending"); - if (null == message) { - pendActionMsg = getI18n().getMessage("message.dist.pending.action", controllerId, distNameVersion); - } - return pendActionMsg; - } - private void updateDistributionInTable(final DistributionSet editedDs) { final Item item = getContainerDataSource().getItem(editedDs.getId()); if (item == null) { @@ -768,4 +736,4 @@ protected String getDeletedEntityName(final Long entityId) { return ""; } -} +} \ No newline at end of file diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTableLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTableLayout.java index c7a4711796..d3645d67fd 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTableLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTableLayout.java @@ -16,6 +16,7 @@ import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetTagManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.UiProperties; import org.eclipse.hawkbit.ui.common.table.AbstractTableLayout; @@ -43,11 +44,11 @@ public DistributionTableLayout(final VaadinMessageSource i18n, final UIEventBus final UINotification notification, final DistributionSetTagManagement distributionSetTagManagement, final TargetTagManagement targetTagManagement, final SystemManagement systemManagement, final TargetManagement targetManagement, final DeploymentManagement deploymentManagement, - final UiProperties uiProperties) { + final TenantConfigurationManagement configManagement, final UiProperties uiProperties) { final DistributionAddUpdateWindowLayout distributionAddUpdateWindowLayout = new DistributionAddUpdateWindowLayout( i18n, notification, eventBus, distributionSetManagement, distributionSetTypeManagement, - systemManagement, entityFactory, null); + systemManagement, entityFactory, null, configManagement); final DsMetadataPopupLayout dsMetadataPopupLayout = new DsMetadataPopupLayout(i18n, notification, eventBus, distributionSetManagement, entityFactory, permissionChecker); @@ -60,7 +61,7 @@ public DistributionTableLayout(final VaadinMessageSource i18n, final UIEventBus distributionTable, new DistributionDetails(i18n, eventBus, permissionChecker, managementUIState, distributionSetManagement, dsMetadataPopupLayout, notification, distributionSetTagManagement, - distributionAddUpdateWindowLayout)); + distributionAddUpdateWindowLayout, configManagement)); } public DistributionTable getDistributionTable() { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/state/ManagementUIState.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/state/ManagementUIState.java index 90c4893765..e09dadaf0d 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/state/ManagementUIState.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/state/ManagementUIState.java @@ -10,9 +10,7 @@ import java.io.Serializable; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; @@ -40,8 +38,6 @@ public class ManagementUIState implements ManagementEntityState, Serializable { private final TargetTableFilters targetTableFilters; - private final Map assignedList = new HashMap<>(); - private final Set deletedDistributionList = new HashSet<>(); private final Set deletedTargetList = new HashSet<>(); @@ -137,10 +133,6 @@ public DistributionTableFilters getDistributionTableFilters() { return distributionTableFilters; } - public Map getAssignedList() { - return assignedList; - } - public Set getDeletedDistributionList() { return deletedDistributionList; } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTable.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTable.java index a424b595be..c2ea3e1404 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTable.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTable.java @@ -33,6 +33,7 @@ import org.eclipse.hawkbit.repository.FilterParams; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetTagManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.event.remote.entity.RemoteEntityEvent; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Tag; @@ -40,6 +41,7 @@ import org.eclipse.hawkbit.repository.model.TargetTag; import org.eclipse.hawkbit.repository.model.TargetTagAssignmentResult; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.UiProperties; import org.eclipse.hawkbit.ui.common.ConfirmationDialog; @@ -115,6 +117,8 @@ public class TargetTable extends AbstractTable { private static final int PROPERTY_DEPT = 3; + private static final String MESSAGE_ASSIGN_TARGET_TO_MULTIPLE_DISTRIBUTIONS = "message.confirm.assign.multiple.entities.multiple.distributions"; + private final transient TargetManagement targetManagement; private final transient DistributionSetManagement distributionSetManagement; @@ -123,6 +127,8 @@ public class TargetTable extends AbstractTable { private final transient DeploymentManagement deploymentManagement; + private final transient TenantConfigurationManagement configManagement; + private final ManagementViewClientCriterion managementViewClientCriterion; private final ManagementUIState managementUIState; @@ -143,7 +149,8 @@ public TargetTable(final UIEventBus eventBus, final VaadinMessageSource i18n, fi final TargetManagement targetManagement, final ManagementUIState managementUIState, final SpPermissionChecker permChecker, final ManagementViewClientCriterion managementViewClientCriterion, final DistributionSetManagement distributionSetManagement, final TargetTagManagement tagManagement, - final DeploymentManagement deploymentManagement, final UiProperties uiProperties) { + final DeploymentManagement deploymentManagement, final TenantConfigurationManagement configManagement, + final UiProperties uiProperties) { super(eventBus, i18n, notification, permChecker); this.targetManagement = targetManagement; this.managementViewClientCriterion = managementViewClientCriterion; @@ -151,6 +158,7 @@ public TargetTable(final UIEventBus eventBus, final VaadinMessageSource i18n, fi this.distributionSetManagement = distributionSetManagement; this.tagManagement = tagManagement; this.deploymentManagement = deploymentManagement; + this.configManagement = configManagement; this.uiProperties = uiProperties; this.actionTypeOptionGroupLayout = new ActionTypeOptionGroupAssignmentLayout(i18n); this.maintenanceWindowLayout = new MaintenanceWindowLayout(i18n); @@ -355,23 +363,18 @@ public AcceptCriterion getDropAcceptCriterion() { private Map prepareQueryConfigFilters() { final Map queryConfig = Maps.newHashMapWithExpectedSize(7); - managementUIState.getTargetTableFilters().getSearchText().ifPresent( - - value -> queryConfig.put(SPUIDefinitions.FILTER_BY_TEXT, value)); - managementUIState.getTargetTableFilters().getDistributionSet().ifPresent( - - value -> queryConfig.put(SPUIDefinitions.FILTER_BY_DISTRIBUTION, value.getId())); - managementUIState.getTargetTableFilters().getPinnedDistId().ifPresent( + managementUIState.getTargetTableFilters().getSearchText() + .ifPresent(value -> queryConfig.put(SPUIDefinitions.FILTER_BY_TEXT, value)); + managementUIState.getTargetTableFilters().getDistributionSet() + .ifPresent(value -> queryConfig.put(SPUIDefinitions.FILTER_BY_DISTRIBUTION, value.getId())); + managementUIState.getTargetTableFilters().getPinnedDistId() + .ifPresent(value -> queryConfig.put(SPUIDefinitions.ORDER_BY_DISTRIBUTION, value)); + managementUIState.getTargetTableFilters().getTargetFilterQuery() + .ifPresent(value -> queryConfig.put(SPUIDefinitions.FILTER_BY_TARGET_FILTER_QUERY, value)); - value -> queryConfig.put(SPUIDefinitions.ORDER_BY_DISTRIBUTION, value)); - managementUIState.getTargetTableFilters().getTargetFilterQuery().ifPresent( - - value -> queryConfig.put(SPUIDefinitions.FILTER_BY_TARGET_FILTER_QUERY, value)); queryConfig.put(SPUIDefinitions.FILTER_BY_NO_TAG, managementUIState.getTargetTableFilters().isNoTagSelected()); - if ( - - isFilteredByTags()) { + if (isFilteredByTags()) { final List list = new ArrayList<>(managementUIState.getTargetTableFilters().getClickedTargetTags()); queryConfig.put(SPUIDefinitions.FILTER_BY_TAG, list.toArray(new String[list.size()])); } @@ -853,13 +856,17 @@ private boolean isFilteredByTags() { private void assignDsToTarget(final DragAndDropEvent event) { final TableTransferable transferable = (TableTransferable) event.getTransferable(); final AbstractTable source = (AbstractTable) transferable.getSourceComponent(); - final Set ids = source.getSelectedEntitiesByTransferable(transferable); - // only one distribution can be assigned to a target - final Long idToSelect = ids.iterator().next(); - selectDraggedEntities(source, new HashSet<>(Arrays.asList(idToSelect))); + + final Set dsIds = filterDistributionSetsToAssign(source.getSelectedEntitiesByTransferable(transferable)); + if (dsIds.isEmpty()) { + getNotification().displayWarning(getI18n().getMessage(DISTRIBUTIONSET_NOT_EXISTS)); + return; + } + + selectDraggedEntities(source, dsIds); final AbstractSelectTargetDetails dropData = (AbstractSelectTargetDetails) event.getTargetDetails(); final Object targetItemId = dropData.getItemIdOver(); - LOG.debug("Adding a log to check if targetItemId is null : {} ", targetItemId); + LOG.debug("Drop target: {} ", targetItemId); if (targetItemId == null) { getNotification().displayWarning(getI18n().getMessage(TARGETS_NOT_EXISTS, "")); return; @@ -873,45 +880,57 @@ private void assignDsToTarget(final DragAndDropEvent event) { return; } - final TargetIdName createTargetIdName = new TargetIdName(target.get()); - final List findDistributionSetById = distributionSetManagement - .get(new HashSet<>(Arrays.asList(idToSelect))); - - if (findDistributionSetById.isEmpty()) { + final List distributionSets = distributionSetManagement.get(dsIds); + if (distributionSets.isEmpty()) { getNotification().displayWarning(getI18n().getMessage(DISTRIBUTIONSET_NOT_EXISTS)); return; } - final DistributionSet distributionSetToAssign = findDistributionSetById.get(0); - addNewTargetToAssignmentList(createTargetIdName, distributionSetToAssign); - openConfirmationWindowForAssignment(distributionSetToAssign.getName(), createTargetIdName.getTargetName()); - } - - private void openConfirmationWindowForAssignment(final String distributionNameToAssign, final String targetName) { - confirmDialog = new ConfirmationDialog(getI18n().getMessage(CAPTION_ENTITY_ASSIGN_ACTION_CONFIRMBOX), - getI18n().getMessage(MESSAGE_CONFIRM_ASSIGN_ENTITY, distributionNameToAssign, "target", targetName), - getI18n().getMessage(UIMessageIdProvider.BUTTON_OK), - getI18n().getMessage(UIMessageIdProvider.BUTTON_CANCEL), ok -> { - if (ok && isMaintenanceWindowValid(maintenanceWindowLayout, getNotification())) { - saveAllAssignments(managementUIState, actionTypeOptionGroupLayout, maintenanceWindowLayout, - deploymentManagement, getNotification(), getEventBus(), getI18n(), this); - } else { - managementUIState.getAssignedList().clear(); - } - }, createAssignmentTab(actionTypeOptionGroupLayout, maintenanceWindowLayout, saveButtonToggle(), - getI18n(), uiProperties), - UIComponentIdProvider.DIST_SET_TO_TARGET_ASSIGNMENT_CONFIRM_ID); + openConfirmationWindowForAssignments(target.get(), distributionSets); + } + + private Set filterDistributionSetsToAssign(final Set ids) { + if (ids.isEmpty()) { + return Collections.emptySet(); + } + if (isMultiAssignmentsEnabled()) { + return new HashSet<>(ids); + } + return Collections.singleton(ids.iterator().next()); + } + + private void openConfirmationWindowForAssignments(final Target target, + final List distributionSets) { + + final String confirmationMessage = getConfirmationMessage(target, distributionSets); + final String caption = getI18n().getMessage(CAPTION_ENTITY_ASSIGN_ACTION_CONFIRMBOX); + final String okLabel = getI18n().getMessage(UIMessageIdProvider.BUTTON_OK); + final String cancelLabel = getI18n().getMessage(UIMessageIdProvider.BUTTON_CANCEL); + + confirmDialog = new ConfirmationDialog(caption, confirmationMessage, okLabel, cancelLabel, ok -> { + if (ok && isMaintenanceWindowValid(maintenanceWindowLayout, getNotification())) { + saveAllAssignments(Collections.singletonList(target), distributionSets, managementUIState, + actionTypeOptionGroupLayout, maintenanceWindowLayout, deploymentManagement, getNotification(), + getEventBus(), getI18n(), this); + } + }, createAssignmentTab(actionTypeOptionGroupLayout, maintenanceWindowLayout, saveButtonToggle(), getI18n(), + uiProperties), UIComponentIdProvider.DIST_SET_TO_TARGET_ASSIGNMENT_CONFIRM_ID); + UI.getCurrent().addWindow(confirmDialog.getWindow()); confirmDialog.getWindow().bringToFront(); } - private Consumer saveButtonToggle() { - return isEnabled -> confirmDialog.getOkButton().setEnabled(isEnabled); + private String getConfirmationMessage(final Target target, final List distributionSets) { + final String targetName = target.getName(); + final int dsCount = distributionSets.size(); + if (dsCount > 1) { + return getI18n().getMessage(MESSAGE_ASSIGN_TARGET_TO_MULTIPLE_DISTRIBUTIONS, dsCount, "target", targetName); + } + return getI18n().getMessage(MESSAGE_CONFIRM_ASSIGN_ENTITY, distributionSets.get(0).getName(), "target", + targetName); } - private void addNewTargetToAssignmentList(final TargetIdName createTargetIdName, - final DistributionSet findDistributionSetAllById) { - final DistributionSetIdName distributionNameId = new DistributionSetIdName(findDistributionSetAllById); - managementUIState.getAssignedList().put(createTargetIdName, distributionNameId); + private Consumer saveButtonToggle() { + return isEnabled -> confirmDialog.getOkButton().setEnabled(isEnabled); } @Override @@ -963,4 +982,11 @@ protected String getDeletedEntityName(final Long entityId) { return ""; } -} + private boolean isMultiAssignmentsEnabled() { + return configManagement + .getConfigurationValue(TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, + Boolean.class) + .getValue(); + } + +} \ No newline at end of file diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RepositoryConfigurationView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RepositoryConfigurationView.java index a41dc53e80..7549fb7835 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RepositoryConfigurationView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RepositoryConfigurationView.java @@ -9,18 +9,22 @@ package org.eclipse.hawkbit.ui.tenantconfiguration; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.ui.UiProperties; import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; import org.eclipse.hawkbit.ui.tenantconfiguration.generic.BooleanConfigurationItem; import org.eclipse.hawkbit.ui.tenantconfiguration.repository.ActionAutocleanupConfigurationItem; import org.eclipse.hawkbit.ui.tenantconfiguration.repository.ActionAutocloseConfigurationItem; +import org.eclipse.hawkbit.ui.tenantconfiguration.repository.MultiAssignmentsConfigurationItem; import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; import com.vaadin.data.Property.ValueChangeEvent; import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.ui.Alignment; import com.vaadin.ui.CheckBox; import com.vaadin.ui.GridLayout; import com.vaadin.ui.Label; +import com.vaadin.ui.Link; import com.vaadin.ui.Panel; import com.vaadin.ui.VerticalLayout; @@ -36,21 +40,30 @@ public class RepositoryConfigurationView extends BaseConfigurationView private final VaadinMessageSource i18n; + private final UiProperties uiProperties; + private final ActionAutocloseConfigurationItem actionAutocloseConfigurationItem; private final ActionAutocleanupConfigurationItem actionAutocleanupConfigurationItem; + private final MultiAssignmentsConfigurationItem multiAssignmentsConfigurationItem; + private CheckBox actionAutocloseCheckBox; private CheckBox actionAutocleanupCheckBox; + private CheckBox multiAssignmentsCheckBox; + RepositoryConfigurationView(final VaadinMessageSource i18n, - final TenantConfigurationManagement tenantConfigurationManagement) { + final TenantConfigurationManagement tenantConfigurationManagement, final UiProperties uiProperties) { this.i18n = i18n; + this.uiProperties = uiProperties; this.actionAutocloseConfigurationItem = new ActionAutocloseConfigurationItem(tenantConfigurationManagement, i18n); this.actionAutocleanupConfigurationItem = new ActionAutocleanupConfigurationItem(tenantConfigurationManagement, i18n); + this.multiAssignmentsConfigurationItem = new MultiAssignmentsConfigurationItem(tenantConfigurationManagement, + i18n); init(); } @@ -70,27 +83,46 @@ private void init() { header.addStyleName("config-panel-header"); vLayout.addComponent(header); - final GridLayout gridLayout = new GridLayout(2, 2); + final GridLayout gridLayout = new GridLayout(3, 3); gridLayout.setSpacing(true); gridLayout.setImmediate(true); gridLayout.setColumnExpandRatio(1, 1.0F); gridLayout.setSizeFull(); + final boolean isMultiAssignmentsEnabled = multiAssignmentsConfigurationItem.isConfigEnabled(); + actionAutocloseCheckBox = SPUIComponentProvider.getCheckBox("", DIST_CHECKBOX_STYLE, null, false, ""); actionAutocloseCheckBox.setId(UIComponentIdProvider.REPOSITORY_ACTIONS_AUTOCLOSE_CHECKBOX); + actionAutocloseCheckBox.setEnabled(!isMultiAssignmentsEnabled); + actionAutocloseConfigurationItem.setEnabled(!isMultiAssignmentsEnabled); actionAutocloseCheckBox.setValue(actionAutocloseConfigurationItem.isConfigEnabled()); actionAutocloseCheckBox.addValueChangeListener(this); actionAutocloseConfigurationItem.addChangeListener(this); gridLayout.addComponent(actionAutocloseCheckBox, 0, 0); gridLayout.addComponent(actionAutocloseConfigurationItem, 1, 0); + multiAssignmentsCheckBox = SPUIComponentProvider.getCheckBox("", DIST_CHECKBOX_STYLE, null, false, ""); + multiAssignmentsCheckBox.setId(UIComponentIdProvider.REPOSITORY_MULTI_ASSIGNMENTS_CHECKBOX); + multiAssignmentsCheckBox.setValue(multiAssignmentsConfigurationItem.isConfigEnabled()); + multiAssignmentsCheckBox.addValueChangeListener(this); + multiAssignmentsCheckBox.setEnabled(!isMultiAssignmentsEnabled); + multiAssignmentsConfigurationItem.setEnabled(!isMultiAssignmentsEnabled); + multiAssignmentsConfigurationItem.addChangeListener(this); + gridLayout.addComponent(multiAssignmentsCheckBox, 0, 1); + gridLayout.addComponent(multiAssignmentsConfigurationItem, 1, 1); + actionAutocleanupCheckBox = SPUIComponentProvider.getCheckBox("", DIST_CHECKBOX_STYLE, null, false, ""); actionAutocleanupCheckBox.setId(UIComponentIdProvider.REPOSITORY_ACTIONS_AUTOCLEANUP_CHECKBOX); actionAutocleanupCheckBox.setValue(actionAutocleanupConfigurationItem.isConfigEnabled()); actionAutocleanupCheckBox.addValueChangeListener(this); actionAutocleanupConfigurationItem.addChangeListener(this); - gridLayout.addComponent(actionAutocleanupCheckBox, 0, 1); - gridLayout.addComponent(actionAutocleanupConfigurationItem, 1, 1); + gridLayout.addComponent(actionAutocleanupCheckBox, 0, 2); + gridLayout.addComponent(actionAutocleanupConfigurationItem, 1, 2); + + final Link linkToProvisioningHelp = SPUIComponentProvider.getHelpLink(i18n, + uiProperties.getLinks().getDocumentation().getProvisioningStateMachine()); + gridLayout.addComponent(linkToProvisioningHelp, 2, 2); + gridLayout.setComponentAlignment(linkToProvisioningHelp, Alignment.BOTTOM_RIGHT); vLayout.addComponent(gridLayout); rootPanel.setContent(vLayout); @@ -101,18 +133,29 @@ private void init() { public void save() { actionAutocloseConfigurationItem.save(); actionAutocleanupConfigurationItem.save(); + multiAssignmentsConfigurationItem.save(); + + final boolean isMultiAssignmentsEnabled = multiAssignmentsConfigurationItem.isConfigEnabled(); + multiAssignmentsCheckBox.setEnabled(!isMultiAssignmentsEnabled); + multiAssignmentsConfigurationItem.setEnabled(!isMultiAssignmentsEnabled); } @Override public boolean isUserInputValid() { return actionAutocloseConfigurationItem.isUserInputValid() - && actionAutocleanupConfigurationItem.isUserInputValid(); + && actionAutocleanupConfigurationItem.isUserInputValid() + && multiAssignmentsConfigurationItem.isUserInputValid(); } @Override public void undo() { + multiAssignmentsConfigurationItem.undo(); + final boolean isMultiAssignmentsEnabled = multiAssignmentsConfigurationItem.isConfigEnabled(); + multiAssignmentsCheckBox.setValue(isMultiAssignmentsEnabled); actionAutocloseConfigurationItem.undo(); actionAutocloseCheckBox.setValue(actionAutocloseConfigurationItem.isConfigEnabled()); + actionAutocloseCheckBox.setEnabled(!isMultiAssignmentsEnabled); + actionAutocloseConfigurationItem.setEnabled(!isMultiAssignmentsEnabled); actionAutocleanupConfigurationItem.undo(); actionAutocleanupCheckBox.setValue(actionAutocleanupConfigurationItem.isConfigEnabled()); } @@ -138,6 +181,10 @@ public void valueChange(final ValueChangeEvent event) { configurationItem = actionAutocloseConfigurationItem; } else if (actionAutocleanupCheckBox.equals(checkBox)) { configurationItem = actionAutocleanupConfigurationItem; + } else if (multiAssignmentsCheckBox.equals(checkBox)) { + configurationItem = multiAssignmentsConfigurationItem; + actionAutocloseCheckBox.setEnabled(!checkBox.getValue()); + actionAutocloseConfigurationItem.setEnabled(!checkBox.getValue()); } else { return; } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RolloutConfigurationView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RolloutConfigurationView.java index 85e6d1eb5f..3352b6a49d 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RolloutConfigurationView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RolloutConfigurationView.java @@ -18,7 +18,7 @@ import com.vaadin.data.Property; import com.vaadin.ui.Alignment; import com.vaadin.ui.CheckBox; -import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.GridLayout; import com.vaadin.ui.Label; import com.vaadin.ui.Link; import com.vaadin.ui.Panel; @@ -61,24 +61,26 @@ private void init() { header.addStyleName("config-panel-header"); vLayout.addComponent(header); - final HorizontalLayout hLayout = new HorizontalLayout(); - hLayout.setSpacing(true); - hLayout.setImmediate(true); + final GridLayout gridLayout = new GridLayout(3, 1); + gridLayout.setSpacing(true); + gridLayout.setImmediate(true); + gridLayout.setColumnExpandRatio(1, 1.0F); + gridLayout.setSizeFull(); approvalCheckbox = SPUIComponentProvider.getCheckBox("", "", null, false, ""); approvalCheckbox.setId(UIComponentIdProvider.ROLLOUT_APPROVAL_ENABLED_CHECKBOX); approvalCheckbox.setValue(approvalConfigurationItem.isConfigEnabled()); approvalCheckbox.addValueChangeListener(this); approvalConfigurationItem.addChangeListener(this); - hLayout.addComponent(approvalCheckbox); - hLayout.addComponent(approvalConfigurationItem); + gridLayout.addComponent(approvalCheckbox, 0, 0); + gridLayout.addComponent(approvalConfigurationItem, 1, 0); final Link linkToApprovalHelp = SPUIComponentProvider.getHelpLink(i18n, uiProperties.getLinks().getDocumentation().getRollout()); - hLayout.addComponent(linkToApprovalHelp); - hLayout.setComponentAlignment(linkToApprovalHelp, Alignment.BOTTOM_RIGHT); + gridLayout.addComponent(linkToApprovalHelp, 2, 0); + gridLayout.setComponentAlignment(linkToApprovalHelp, Alignment.BOTTOM_RIGHT); - vLayout.addComponent(hLayout); + vLayout.addComponent(gridLayout); rootPanel.setContent(vLayout); setCompositionRoot(rootPanel); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/TenantConfigurationDashboardView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/TenantConfigurationDashboardView.java index a6477fe46d..dd2b596c2d 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/TenantConfigurationDashboardView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/TenantConfigurationDashboardView.java @@ -91,7 +91,8 @@ public class TenantConfigurationDashboardView extends CustomComponent implements securityTokenGenerator, uiProperties); this.pollingConfigurationView = new PollingConfigurationView(i18n, controllerPollProperties, tenantConfigurationManagement); - this.repositoryConfigurationView = new RepositoryConfigurationView(i18n, tenantConfigurationManagement); + this.repositoryConfigurationView = new RepositoryConfigurationView(i18n, tenantConfigurationManagement, + uiProperties); this.rolloutConfigurationView = new RolloutConfigurationView(i18n, tenantConfigurationManagement, uiProperties); this.i18n = i18n; diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/repository/MultiAssignmentsConfigurationItem.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/repository/MultiAssignmentsConfigurationItem.java new file mode 100644 index 0000000000..83dc32b765 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/repository/MultiAssignmentsConfigurationItem.java @@ -0,0 +1,111 @@ +/** + * 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.ui.tenantconfiguration.repository; + +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; +import org.eclipse.hawkbit.ui.common.builder.LabelBuilder; +import org.eclipse.hawkbit.ui.tenantconfiguration.generic.AbstractBooleanTenantConfigurationItem; +import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; + +import com.vaadin.ui.Label; +import com.vaadin.ui.VerticalLayout; + +/** + * This class represents the UI item for enabling /disabling the + * Multi-Assignments feature as part of the repository configuration view. + */ +public class MultiAssignmentsConfigurationItem extends AbstractBooleanTenantConfigurationItem { + + private static final long serialVersionUID = 1L; + + private static final String MSG_KEY_CHECKBOX = "label.configuration.repository.multiassignments"; + private static final String MSG_KEY_NOTICE = "label.configuration.repository.multiassignments.notice"; + + private final VerticalLayout container; + private final VaadinMessageSource i18n; + + private boolean isMultiAssignmentsEnabled; + private boolean multiAssignmentsEnabledChanged; + + /** + * Constructor. + * + * @param tenantConfigurationManagement + * to read /write tenant-specific configuration properties + * @param i18n + * to obtain localized strings + */ + public MultiAssignmentsConfigurationItem(final TenantConfigurationManagement tenantConfigurationManagement, + final VaadinMessageSource i18n) { + super(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, tenantConfigurationManagement, i18n); + this.i18n = i18n; + + super.init(MSG_KEY_CHECKBOX); + isMultiAssignmentsEnabled = isConfigEnabled(); + + container = new VerticalLayout(); + container.setImmediate(true); + + container.addComponent(newLabel(MSG_KEY_NOTICE)); + + if (isMultiAssignmentsEnabled) { + setSettingsVisible(isMultiAssignmentsEnabled); + } + + } + + @Override + public void configEnable() { + if (!isMultiAssignmentsEnabled) { + multiAssignmentsEnabledChanged = true; + } + isMultiAssignmentsEnabled = true; + setSettingsVisible(true); + } + + @Override + public void configDisable() { + if (isMultiAssignmentsEnabled) { + multiAssignmentsEnabledChanged = true; + } + isMultiAssignmentsEnabled = false; + setSettingsVisible(false); + } + + @Override + public void save() { + if (!multiAssignmentsEnabledChanged) { + return; + } + getTenantConfigurationManagement().addOrUpdateConfiguration(getConfigurationKey(), isMultiAssignmentsEnabled); + } + + @Override + public void undo() { + multiAssignmentsEnabledChanged = false; + isMultiAssignmentsEnabled = getTenantConfigurationManagement() + .getConfigurationValue(getConfigurationKey(), Boolean.class).getValue(); + } + + private void setSettingsVisible(final boolean visible) { + if (visible) { + addComponent(container); + } else { + removeComponent(container); + } + } + + private Label newLabel(final String msgKey) { + final Label label = new LabelBuilder().name(i18n.getMessage(msgKey)).buildLabel(); + label.setWidthUndefined(); + return label; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java index 2c34c3fdef..b96e67f485 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java @@ -608,6 +608,11 @@ public final class UIComponentIdProvider { */ public static final String DETAILS_TYPE_LABEL_ID = "details.type"; + /** + * Table details Required Migration Step label id. + */ + public static final String DETAILS_REQUIRED_MIGRATION_STEP_LABEL_ID = "details.required.migration.step"; + /** * Id of show filter button in software module table. */ @@ -1154,6 +1159,11 @@ public final class UIComponentIdProvider { */ public static final String ROLLOUT_POPUP_ID = "add.update.rollout.popup"; + /** + * Create popup id. + */ + public static final String CREATE_POPUP_ID = "create.popup.id"; + /** * DistributionSet table details tab id in Distributions . */ @@ -1240,6 +1250,12 @@ public final class UIComponentIdProvider { */ public static final String REPOSITORY_ACTIONS_AUTOCLEANUP_CHECKBOX = "repositoryactionsautocleanupcheckbox"; + /** + * Configuration checkbox for + * {@link TenantConfigurationKey#MULTI_ASSIGNMENTS_ENABLED}. + */ + public static final String REPOSITORY_MULTI_ASSIGNMENTS_CHECKBOX = "repositorymultiassignmentscheckbox"; + /** * Configuration checkbox for * {@link TenantConfigurationKey#ROLLOUT_APPROVAL_ENABLED} @@ -1279,4 +1295,4 @@ public final class UIComponentIdProvider { private UIComponentIdProvider() { } -} +} \ No newline at end of file diff --git a/hawkbit-ui/src/main/resources/messages.properties b/hawkbit-ui/src/main/resources/messages.properties index d138cddc3b..590bf4af94 100644 --- a/hawkbit-ui/src/main/resources/messages.properties +++ b/hawkbit-ui/src/main/resources/messages.properties @@ -242,6 +242,8 @@ label.configuration.repository.autocleanup.action.suffix = day(s) label.configuration.repository.autocleanup.action.expiry.invalid = The specified number of days is invalid. Please enter a positive integer value between 1 and 1000. label.configuration.anonymous.download = Allow targets to download artifacts without security credentials label.configuration.repository.autocleanup.action.notice = Warning: The actions are deleted from the repository and cannot be restored +label.configuration.repository.multiassignments = Allow parallel execution of multiple distribution set assignments and rollouts +label.configuration.repository.multiassignments.notice = Warning: Once this assignment behavior is active, it cannot be deactivated. label.unsupported.browser.ie = Sorry! Your current browser is not supported. Please use Internet Explorer 11 and above label.auto.assign.description = When an auto assign distribution set is selected, it will be automatically assigned to all targets that match the target filter. label.auto.assign.enable = Enable auto assignment @@ -707,6 +709,7 @@ message.confirm.delete.entity = Are you sure you want to delete {0} {1}{2}? caption.entity.assign.action.confirmbox = Confirm Assignment message.confirm.assign.entity = Are you sure you want to assign distribution {0} to {1} {2}? message.confirm.assign.multiple.entities = Are you sure you want to assign {0} {1} to distribution {2}? +message.confirm.assign.multiple.entities.multiple.distributions = Are you sure you want to assign {0} distributions to {1} {2}? # character descriptions character.whitespace = whitespace