diff --git a/docs/content/ui.md b/docs/content/ui.md index 63fa86ab1..caf243a62 100644 --- a/docs/content/ui.md +++ b/docs/content/ui.md @@ -142,7 +142,7 @@ name==CCU* and updatestatus==pending ![Target Filter Management view](../images/ui/target_filter.png) ### Auto assignment -It is possible to assign some distribution set with different action types (_forced_ or _soft_) to all targets that belong to the corresponding custom target filter, including the ones, that are registered later on. +It is possible to assign some distribution set with different action types (_forced_, _soft_, or _download only_) to all targets that belong to the corresponding custom target filter, including the ones, that are registered later on. In order to activate the auto-assignment, one should first click on _Auto assignment_ cell in Custom Filters table, and then check the corresponding checkbox. After that, the action type and distribution set for auto-assignment should be selected and confirmed. diff --git a/docs/static/images/ui/target_filter_auto_assignment.png b/docs/static/images/ui/target_filter_auto_assignment.png index 2b195bd4c..a6a5c0168 100644 Binary files a/docs/static/images/ui/target_filter_auto_assignment.png and b/docs/static/images/ui/target_filter_auto_assignment.png differ 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 c7cc34949..e685ba590 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 @@ -225,7 +225,7 @@ public enum SpServerError { * invalid. */ SP_AUTO_ASSIGN_ACTION_TYPE_INVALID("hawkbit.server.error.repo.invalidAutoAssignActionType", - "The given action type for auto-assignment is invalid: allowed values are FORCED and SOFT"), + "The given action type for auto-assignment is invalid: allowed values are ['forced', 'soft', 'downloadonly']"), /** * Error message informing that the distribution set for auto-assignment is diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpAuthenticationMessageHandler.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpAuthenticationMessageHandler.java index ee07e726a..a5e3a100a 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpAuthenticationMessageHandler.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpAuthenticationMessageHandler.java @@ -133,7 +133,6 @@ public class AmqpAuthenticationMessageHandler extends BaseAmqpService { checkByTargetId(sha1Hash, secruityToken.getTargetId()); } else { LOG.info("anonymous download no authentication check for artifact {}", sha1Hash); - return; } } 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 7b0674c88..623f64ea1 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 @@ -270,6 +270,12 @@ public class AmqpConfiguration { return new DefaultAmqpMessageSenderService(rabbitTemplate()); } + /** + * Create RabbitListenerContainerFactory bean if no listenerContainerFactory + * bean found + * + * @return RabbitListenerContainerFactory bean + */ @Bean @ConditionalOnMissingBean(name = "listenerContainerFactory") public RabbitListenerContainerFactory listenerContainerFactory( 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 080c29303..8fb6f8bbb 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 @@ -38,6 +38,8 @@ import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEv import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.CancelTargetAssignmentEvent; +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.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; @@ -143,28 +145,28 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { PageRequest.of(0, RepositoryConstants.MAX_META_DATA_COUNT), module.getId()) .getContent())); - targetManagement.getByControllerID(assignedEvent.getActions().keySet()) - .forEach(target -> sendUpdateMessageToTarget(assignedEvent.getTenant(), target, - assignedEvent.getActions().get(target.getControllerId()), modules, - assignedEvent.isMaintenanceWindowAvailable())); + targetManagement.getByControllerID(assignedEvent.getActions().keySet()).forEach( + target -> sendUpdateMessageToTarget(assignedEvent.getActions().get(target.getControllerId()), + target, modules)); }); } /** - * Method to get the type of event depending on whether the action 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 + * 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. * - * @param maintenanceWindowAvailable - * valid maintenance window or not. + * @param action + * current action properties. * * @return {@link EventTopic} to use for message. */ - private static EventTopic getEventTypeForTarget(final boolean maintenanceWindowAvailable) { - return maintenanceWindowAvailable ? EventTopic.DOWNLOAD_AND_INSTALL : EventTopic.DOWNLOAD; + private static EventTopic getEventTypeForTarget(final ActionProperties action) { + return (Action.ActionType.DOWNLOAD_ONLY.equals(action.getActionType()) || + !action.isMaintenanceWindowAvailable()) ? EventTopic.DOWNLOAD : EventTopic.DOWNLOAD_AND_INSTALL; } /** @@ -206,8 +208,10 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { updateAttributesEvent.getTargetAddress()); } - protected void sendUpdateMessageToTarget(final String tenant, final Target target, final Long actionId, - final Map> modules, final boolean maintenanceWindowAvailable) { + protected void sendUpdateMessageToTarget(final ActionProperties action, final Target target, + final Map> modules) { + + String tenant = action.getTenant(); final URI targetAdress = target.getAddress(); if (!IpUtil.isAmqpUri(targetAdress)) { @@ -215,7 +219,7 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { } final DmfDownloadAndUpdateRequest downloadAndUpdateRequest = new DmfDownloadAndUpdateRequest(); - downloadAndUpdateRequest.setActionId(actionId); + downloadAndUpdateRequest.setActionId(action.getId()); final String targetSecurityToken = systemSecurityContext.runAsSystem(target::getSecurityToken); downloadAndUpdateRequest.setTargetSecurityToken(targetSecurityToken); @@ -227,8 +231,7 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { }); final Message message = getMessageConverter().toMessage(downloadAndUpdateRequest, - createConnectorMessagePropertiesEvent(tenant, target.getControllerId(), - getEventTypeForTarget(maintenanceWindowAvailable))); + createConnectorMessagePropertiesEvent(tenant, target.getControllerId(), getEventTypeForTarget(action))); amqpSenderService.sendMessage(message, targetAdress); } 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 1cc6752c9..feccb9d7a 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 @@ -31,6 +31,7 @@ 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.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; import org.eclipse.hawkbit.repository.model.Target; @@ -218,8 +219,8 @@ public class AmqpMessageHandlerService extends BaseAmqpService { action.getDistributionSet().getModules().forEach(module -> modules.put(module, metadata.get(module.getId()))); - amqpMessageDispatcherService.sendUpdateMessageToTarget(action.getTenant(), action.getTarget(), action.getId(), - modules, action.isMaintenanceWindowAvailable()); + amqpMessageDispatcherService.sendUpdateMessageToTarget(new ActionProperties(action), action.getTarget(), + modules); } /** 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 a289756cf..236fa43d8 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 @@ -11,7 +11,6 @@ package org.eclipse.hawkbit.amqp; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -46,6 +45,7 @@ import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder; 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.Artifact; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Target; @@ -301,7 +301,7 @@ public class AmqpMessageHandlerServiceTest { final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT); final Message message = new Message(new byte[0], messageProperties); try { - amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); + amqpMessageHandlerService.onMessage(message, "unknownMessageType", TENANT, "vHost"); fail("AmqpRejectAndDontRequeueException was excepeted due to unknown message type"); } catch (final AmqpRejectAndDontRequeueException e) { } @@ -462,19 +462,16 @@ public class AmqpMessageHandlerServiceTest { // test amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); - final ArgumentCaptor tenantCaptor = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor actionPropertiesCaptor = ArgumentCaptor.forClass(ActionProperties.class); final ArgumentCaptor targetCaptor = ArgumentCaptor.forClass(Target.class); - final ArgumentCaptor actionIdCaptor = ArgumentCaptor.forClass(Long.class); - verify(amqpMessageDispatcherServiceMock, times(1)).sendUpdateMessageToTarget(tenantCaptor.capture(), - targetCaptor.capture(), actionIdCaptor.capture(), any(Map.class), anyBoolean()); - final String tenant = tenantCaptor.getValue(); - final String controllerId = targetCaptor.getValue().getControllerId(); - final Long actionId = actionIdCaptor.getValue(); - - assertThat(tenant).as("event has tenant").isEqualTo("DEFAULT"); - assertThat(controllerId).as("event has wrong controller id").isEqualTo("target1"); - assertThat(actionId).as("event has wrong action id").isEqualTo(22L); + 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"); + assertThat(targetCaptor.getValue().getControllerId()).as("event has wrong controller id").isEqualTo("target1"); + assertThat(actionProperties.getId()).as("event has wrong action id").isEqualTo(22L); } @@ -516,6 +513,7 @@ public class AmqpMessageHandlerServiceTest { when(actionMock.getId()).thenReturn(targetId); when(actionMock.getTenant()).thenReturn("DEFAULT"); when(actionMock.getTarget()).thenReturn(targetMock); + when(actionMock.getActionType()).thenReturn(Action.ActionType.SOFT); when(targetMock.getControllerId()).thenReturn("target1"); return actionMock; } 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 21d50b68f..f5b9c0c42 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 @@ -34,6 +34,7 @@ import org.eclipse.hawkbit.matcher.SoftwareModuleJsonMatcher; import org.eclipse.hawkbit.rabbitmq.test.AbstractAmqpIntegrationTest; import org.eclipse.hawkbit.rabbitmq.test.AmqpTestConfiguration; import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.SoftwareModule; @@ -235,7 +236,7 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt assertThat(targetManagement.count()).isEqualTo(expectedTargetsCount); } - private Message assertReplyMessageHeader(final EventTopic eventTopic, final String controllerId) { + protected Message assertReplyMessageHeader(final EventTopic eventTopic, final String controllerId) { verifyReplyToListener(); final Message replyMessage = replyToListener.getEventTopicMessages().get(eventTopic); assertAllTargetsCount(1); @@ -379,4 +380,16 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt return AmqpSettings.DMF_EXCHANGE; } + @Step + protected DistributionSet createTargetAndDistributionSetAndAssign(final String controllerId, + final Action.ActionType actionType) { + registerAndAssertTargetWithExistingTenant(controllerId); + + final DistributionSet distributionSet = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + testdataFactory.addSoftwareModuleMetadata(distributionSet); + + assignDistributionSet(distributionSet.getId(), controllerId, actionType); + return distributionSet; + } + } 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 be8da4674..9f177cf05 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,10 +8,12 @@ */ package org.eclipse.hawkbit.integration; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.Callable; +import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; @@ -25,6 +27,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedE import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.Target; @@ -32,12 +35,18 @@ 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.junit.Test; +import org.mockito.Mockito; import org.springframework.amqp.core.Message; import io.qameta.allure.Description; 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 { @@ -198,6 +207,37 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer assertRequestAttributesUpdateMessage(controllerId); } + @Test + @Description("Tests the download_only assignment: asserts correct dmf Message topic, and assigned DS") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 6), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) }) + 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()); + + assertThat(message).isNotNull(); + final Map headers = message.getMessageProperties().getHeaders(); + assertThat(headers).containsEntry("thingId", controllerId); + assertThat(headers).containsEntry("type", EVENT.toString()); + assertThat(headers).containsEntry("topic", DOWNLOAD.toString()); + + final Optional target = controllerManagement.getByControllerId(controllerId); + assertThat(target).isPresent(); + + // verify the DS was assigned to the Target + final DistributionSet assignedDistributionSet = ((JpaTarget) target.get()).getAssignedDistributionSet(); + assertThat(assignedDistributionSet.getId()).isEqualTo(distributionSet.getId()); + } + private void waitUntilTargetHasStatus(final String controllerId, final TargetUpdateStatus status) { waitUntil(() -> { final Optional findTargetByControllerID = targetManagement.getByControllerID(controllerId); 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 3716c0932..41519072f 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 @@ -9,14 +9,20 @@ package org.eclipse.hawkbit.integration; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY; +import java.io.IOException; +import java.nio.charset.Charset; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.eclipse.hawkbit.amqp.AmqpProperties; import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; @@ -37,6 +43,8 @@ import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedE import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; +import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -797,6 +805,99 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic verifyNumberOfDeadLetterMessages(3); } + + @Test + @Description("Tests the download_only assignment: tests the handling of a target reporting DOWNLOADED") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionUpdatedEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 6), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetAttributesRequestedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), @Expect(type = TargetPollEvent.class, count = 1) }) + public void downloadOnlyAssignmentFinishesActionWhenTargetReportsDownloaded() + throws IOException { + // create target + 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()); + + // get actionId from Message + Long actionId = Long.parseLong(getJsonFieldFromBody(message.getBody(), "actionId")); + + // Send DOWNLOADED message + sendActionUpdateStatus(new DmfActionUpdateStatus(actionId, DmfActionStatus.DOWNLOADED)); + assertAction(actionId, 1, Status.RUNNING, Status.DOWNLOADED); + Mockito.verifyZeroInteractions(getDeadletterListener()); + + verifyAssignedDsAndInstalledDs(controllerId, distributionSet.getId(), null); + } + + @Test + @Description("Tests the download_only assignment: tests the handling of a target reporting FINISHED") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionUpdatedEvent.class, count = 2), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 6), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetAttributesRequestedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 3), + @Expect(type = TargetPollEvent.class, count = 1) }) + public void downloadOnlyAssignmentAllowsActionStatusUpdatesWhenTargetReportsFinishedAndUpdatesInstalledDS() + throws IOException { + + // create target + 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()); + + // get actionId from Message + Long actionId = Long.parseLong(getJsonFieldFromBody(message.getBody(), "actionId")); + + // Send DOWNLOADED message, should result in the action being closed + sendActionUpdateStatus(new DmfActionUpdateStatus(actionId, DmfActionStatus.DOWNLOADED)); + assertAction(actionId, 1, Status.RUNNING, Status.DOWNLOADED); + Mockito.verifyZeroInteractions(getDeadletterListener()); + + verifyAssignedDsAndInstalledDs(controllerId, distributionSet.getId(), null); + + // Send FINISHED message + sendActionUpdateStatus(new DmfActionUpdateStatus(actionId, DmfActionStatus.FINISHED)); + assertAction(actionId, 2, Status.RUNNING, Status.DOWNLOADED, Status.FINISHED); + Mockito.verifyZeroInteractions(getDeadletterListener()); + + verifyAssignedDsAndInstalledDs(controllerId, distributionSet.getId(), distributionSet.getId()); + } + + @Step + private void verifyAssignedDsAndInstalledDs(final String controllerId, final Long assignedDsId, + final Long installedDsId) { + final Optional target = controllerManagement.getByControllerId(controllerId); + assertThat(target).isPresent(); + + // verify the DS was assigned to the Target + final DistributionSet assignedDistributionSet = ((JpaTarget) target.get()).getAssignedDistributionSet(); + assertThat(assignedDsId).isNotNull(); + assertThat(assignedDistributionSet.getId()).isEqualTo(assignedDsId); + + // verify that the installed DS was not affected + final JpaDistributionSet installedDistributionSet = ((JpaTarget) target.get()).getInstalledDistributionSet(); + if (installedDsId == null) { + assertThat(installedDistributionSet).isNull(); + } else { + assertThat(installedDistributionSet.getId()).isEqualTo(installedDsId); + } + } + private void sendUpdateAttributesMessageWithGivenAttributes(final String target, final String key, final String value) { final DmfAttributeUpdate controllerAttribute = new DmfAttributeUpdate(); @@ -887,4 +988,11 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic createConditionFactory().untilAsserted(() -> Mockito .verify(getDeadletterListener(), Mockito.times(numberOfInvocations)).handleMessage(Mockito.any())); } + + private static String getJsonFieldFromBody(final byte[] body, final String fieldName) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + final ObjectNode node = objectMapper.readValue(new String(body, Charset.defaultCharset()), ObjectNode.class); + assertThat(node.has(fieldName)).isTrue(); + return node.get(fieldName).asText(); + } } diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfActionUpdateStatus.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfActionUpdateStatus.java index ac48c002e..1b6c681e8 100644 --- a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfActionUpdateStatus.java +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfActionUpdateStatus.java @@ -34,8 +34,8 @@ public class DmfActionUpdateStatus { @JsonProperty private List message; - public DmfActionUpdateStatus(@JsonProperty(value = "actionId", required = true) Long actionId, - @JsonProperty(value = "actionStatus", required = true) DmfActionStatus actionStatus) { + public DmfActionUpdateStatus(@JsonProperty(value = "actionId", required = true) final Long actionId, + @JsonProperty(value = "actionStatus", required = true) final DmfActionStatus actionStatus) { this.actionId = actionId; this.actionStatus = actionStatus; } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryProperties.java index 6579e80b9..46e563cda 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryProperties.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryProperties.java @@ -23,9 +23,12 @@ public class RepositoryProperties { /** * Set to true if the repository has to reject - * {@link ActionStatus} entries for actions that are closed. Note: if this - * is enforced you have to make sure that the feedback channel from the - * devices i in order. + * {@link ActionStatus} entries for actions that are closed. This is + * especially useful if the action status feedback channel order from the + * device cannot be guaranteed. + * + * Note: if this is enforced you have to make sure that the feedback + * channel from the devices is in order. */ private boolean rejectActionStatusForClosedAction; diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/TargetAssignDistributionSetEvent.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/TargetAssignDistributionSetEvent.java index d57742d48..a61b60770 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/TargetAssignDistributionSetEvent.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/TargetAssignDistributionSetEvent.java @@ -8,13 +8,14 @@ */ package org.eclipse.hawkbit.repository.event.remote; -import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.ActionProperties; /** * TenantAwareEvent that gets sent when a distribution set gets assigned to a @@ -28,7 +29,7 @@ public class TargetAssignDistributionSetEvent extends RemoteTenantAwareEvent { private boolean maintenanceWindowAvailable; - private final Map actions = new HashMap<>(); + private final Map actions = new HashMap<>(); /** * Default constructor. @@ -57,12 +58,20 @@ public class TargetAssignDistributionSetEvent extends RemoteTenantAwareEvent { this.distributionSetId = distributionSetId; this.maintenanceWindowAvailable = maintenanceWindowAvailable; actions.putAll(a.stream().filter(action -> action.getDistributionSet().getId().longValue() == distributionSetId) - .collect(Collectors.toMap(action -> action.getTarget().getControllerId(), Action::getId))); + .collect(Collectors.toMap(action -> action.getTarget().getControllerId(), ActionProperties::new))); } + /** + * Constructor. + * + * @param action + * the action created for this assignment + * @param applicationId + * the application id + */ public TargetAssignDistributionSetEvent(final Action action, final String applicationId) { - this(action.getTenant(), action.getDistributionSet().getId(), Arrays.asList(action), applicationId, + this(action.getTenant(), action.getDistributionSet().getId(), Collections.singletonList(action), applicationId, action.isMaintenanceWindowAvailable()); } @@ -74,7 +83,7 @@ public class TargetAssignDistributionSetEvent extends RemoteTenantAwareEvent { return maintenanceWindowAvailable; } - public Map getActions() { + public Map getActions() { return actions; } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java index 42835a0ea..996b06919 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java @@ -229,7 +229,12 @@ public interface Action extends TenantAwareBaseEntity { * {@link Action#isHitAutoForceTime(long)} is reached, {@link #FORCED} * after that. */ - TIMEFORCED; + TIMEFORCED, + + /** + * Target is only advised to download, but not install + */ + DOWNLOAD_ONLY; } /** diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionProperties.java new file mode 100644 index 000000000..489c326c7 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionProperties.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2019 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +/** + * Holds properties for {@link Action} + */ +public class ActionProperties implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private Action.ActionType actionType; + private String tenant; + private boolean maintenanceWindowAvailable; + + public ActionProperties() { + } + + /** + * Constructor + * @param action + * the action to populate the properties from + */ + public ActionProperties(final Action action) { + this.id = action.getId(); + this.actionType = action.getActionType(); + this.tenant = action.getTenant(); + this.maintenanceWindowAvailable = action.isMaintenanceWindowAvailable(); + } + + public void setId(final Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setTenant(final String tenant) { + this.tenant = tenant; + } + + public String getTenant() { + return tenant; + } + + public void setMaintenanceWindowAvailable(final boolean maintenanceWindowAvailable) { + this.maintenanceWindowAvailable = maintenanceWindowAvailable; + } + + public boolean isMaintenanceWindowAvailable() { + return maintenanceWindowAvailable; + } + + public Action.ActionType getActionType() { + return actionType; + } + + public void setActionType(final Action.ActionType actionType) { + this.actionType = actionType; + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java index d8f7ab2ca..987880a33 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java @@ -48,7 +48,7 @@ public interface TargetFilterQuery extends TenantAwareBaseEntity { * Allowed values for auto-assign action type */ Set ALLOWED_AUTO_ASSIGN_ACTION_TYPES = Collections - .unmodifiableSet(EnumSet.of(ActionType.FORCED, ActionType.SOFT)); + .unmodifiableSet(EnumSet.of(ActionType.FORCED, ActionType.SOFT, ActionType.DOWNLOAD_ONLY)); /** * @return name of the {@link TargetFilterQuery}. diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatus.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatus.java index eed7d7bb2..340b3173c 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatus.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatus.java @@ -58,6 +58,7 @@ public class TotalTargetCountStatus { private final Map statusTotalCountMap = new EnumMap<>(Status.class); private final Long totalTargetCount; + private final Action.ActionType rolloutType; /** * Create a new states map with the target count for each state. @@ -66,11 +67,14 @@ public class TotalTargetCountStatus { * the action state map * @param totalTargetCount * the total target count + * @param rolloutType + * the type of the rollout */ public TotalTargetCountStatus(final List targetCountActionStatus, - final Long totalTargetCount) { + final Long totalTargetCount, final Action.ActionType rolloutType) { this.totalTargetCount = totalTargetCount; - mapActionStatusToTotalTargetCountStatus(targetCountActionStatus); + this.rolloutType = rolloutType; + addToTotalCount(targetCountActionStatus); } /** @@ -78,9 +82,11 @@ public class TotalTargetCountStatus { * * @param totalTargetCount * the total target count + * @param rolloutType + * the type of the rollout */ - public TotalTargetCountStatus(final Long totalTargetCount) { - this(Collections.emptyList(), totalTargetCount); + public TotalTargetCountStatus(final Long totalTargetCount, final Action.ActionType rolloutType) { + this(Collections.emptyList(), totalTargetCount, rolloutType); } /** @@ -119,8 +125,7 @@ public class TotalTargetCountStatus { * @param rolloutStatusCountItems * all target {@link Status} with total count */ - private final void mapActionStatusToTotalTargetCountStatus( - final List targetCountActionStatus) { + private void addToTotalCount(final List targetCountActionStatus) { if (targetCountActionStatus == null) { statusTotalCountMap.put(TotalTargetCountStatus.Status.NOTSTARTED, totalTargetCount); return; @@ -128,40 +133,40 @@ public class TotalTargetCountStatus { statusTotalCountMap.put(Status.RUNNING, 0L); Long notStartedTargetCount = totalTargetCount; for (final TotalTargetCountActionStatus item : targetCountActionStatus) { - convertStatus(item); + addToTotalCount(item); notStartedTargetCount -= item.getCount(); } statusTotalCountMap.put(TotalTargetCountStatus.Status.NOTSTARTED, notStartedTargetCount); } + private void addToTotalCount(final TotalTargetCountActionStatus item) { + final Status status = convertStatus(item.getStatus()); + statusTotalCountMap.merge(status, item.getCount(), Long::sum); + } + // Exception squid:MethodCyclomaticComplexity - simple state conversion, not // really complex. @SuppressWarnings("squid:MethodCyclomaticComplexity") - private void convertStatus(final TotalTargetCountActionStatus item) { - switch (item.getStatus()) { + private Status convertStatus(final Action.Status status){ + switch (status) { case SCHEDULED: - statusTotalCountMap.put(Status.SCHEDULED, item.getCount()); - break; + return Status.SCHEDULED; case ERROR: - statusTotalCountMap.put(Status.ERROR, item.getCount()); - break; + return Status.ERROR; case FINISHED: - statusTotalCountMap.put(Status.FINISHED, item.getCount()); - break; + return Status.FINISHED; + case CANCELED: + return Status.CANCELLED; case RETRIEVED: case RUNNING: case WARNING: case DOWNLOAD: - case DOWNLOADED: case CANCELING: - final Long runningItemsCount = statusTotalCountMap.get(Status.RUNNING) + item.getCount(); - statusTotalCountMap.put(Status.RUNNING, runningItemsCount); - break; - case CANCELED: - statusTotalCountMap.put(Status.CANCELLED, item.getCount()); - break; + return Status.RUNNING; + case DOWNLOADED: + return Action.ActionType.DOWNLOAD_ONLY.equals(rolloutType) ? Status.FINISHED : Status.RUNNING; default: - throw new IllegalArgumentException("State " + item.getStatus() + "is not valid"); + throw new IllegalArgumentException("State " + status + "is not valid"); } } diff --git a/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatusTest.java b/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatusTest.java new file mode 100644 index 000000000..96d9bf1ee --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatusTest.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2019 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Feature("Component Tests - TotalTargetCountStatus") +@Story("TotalTargetCountStatus should correctly present finished DOWNLOAD_ONLY actions") +public class TotalTargetCountStatusTest { + + private final List targetCountActionStatuses = Arrays.asList( + new TotalTargetCountActionStatus(Action.Status.SCHEDULED, 1L), + new TotalTargetCountActionStatus(Action.Status.ERROR, 2L), + new TotalTargetCountActionStatus(Action.Status.FINISHED, 3L), + new TotalTargetCountActionStatus(Action.Status.CANCELED, 4L), + new TotalTargetCountActionStatus(Action.Status.RETRIEVED, 5L), + new TotalTargetCountActionStatus(Action.Status.RUNNING, 6L), + new TotalTargetCountActionStatus(Action.Status.WARNING, 7L), + new TotalTargetCountActionStatus(Action.Status.DOWNLOAD, 8L), + new TotalTargetCountActionStatus(Action.Status.CANCELING, 9L), + new TotalTargetCountActionStatus(Action.Status.DOWNLOADED, 10L)); + + @Test + @Description("Different Action Statuses should be correctly mapped to the corresponding " + + "TotalTargetCountStatus.Status") + public void shouldCorrectlyMapActionStatuses() { + TotalTargetCountStatus status = new TotalTargetCountStatus(targetCountActionStatuses, 55L, + Action.ActionType.FORCED); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.SCHEDULED)).isEqualTo(1L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.ERROR)).isEqualTo(2L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.FINISHED)).isEqualTo(3L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.CANCELLED)).isEqualTo(4L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.RUNNING)).isEqualTo(45L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.NOTSTARTED)).isEqualTo(0L); + assertThat(status.getFinishedPercent()).isEqualTo((float) 100 * 3 / 55); + } + + @Test + @Description("When an empty list is passed to the TotalTargetCountStatus, all actions should be displayed as " + + "NOTSTARTED") + public void shouldCorrectlyMapActionStatusesToNotStarted() { + TotalTargetCountStatus status = new TotalTargetCountStatus(Collections.emptyList(), 55L, + Action.ActionType.FORCED); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.SCHEDULED)).isEqualTo(0L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.ERROR)).isEqualTo(0L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.FINISHED)).isEqualTo(0L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.CANCELLED)).isEqualTo(0L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.RUNNING)).isEqualTo(0L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.NOTSTARTED)).isEqualTo(55L); + assertThat(status.getFinishedPercent()).isEqualTo(0); + } + + @Test + @Description("DownloadOnly actions should be displayed as FINISHED when they have ActionStatus.DOWNLOADED") + public void shouldCorrectlyMapActionStatusesInDownloadOnlyCase() { + TotalTargetCountStatus status = new TotalTargetCountStatus(targetCountActionStatuses, 55L, + Action.ActionType.DOWNLOAD_ONLY); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.SCHEDULED)).isEqualTo(1L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.ERROR)).isEqualTo(2L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.FINISHED)).isEqualTo(13L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.CANCELLED)).isEqualTo(4L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.RUNNING)).isEqualTo(35L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.NOTSTARTED)).isEqualTo(0L); + assertThat(status.getFinishedPercent()).isEqualTo((float) 100 * 13 / 55); + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java index 8824a01dd..9abfa563e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java @@ -313,17 +313,13 @@ public interface ActionRepository extends BaseEntityRepository, * the rollout the actions are belong to * @param rolloutGroup * the rolloutgroup the actions are belong to - * @param notStatus1 - * the status the action should not have - * @param notStatus2 - * the status the action should not have - * @param notStatus3 - * the status the action should not have + * @param statuses + * the list of statuses the action should not have * @return the count of actions referring the rollout and rolloutgroup and * are not in given states */ - Long countByRolloutAndRolloutGroupAndStatusNotAndStatusNotAndStatusNot(JpaRollout rollout, - JpaRolloutGroup rolloutGroup, Status notStatus1, Status notStatus2, Status notStatus3); + Long countByRolloutAndRolloutGroupAndStatusNotIn(JpaRollout rollout, JpaRolloutGroup rolloutGroup, + List statuses); /** * Counts all actions referring to a given rollout and rolloutgroup. 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 e914c0a3c..14e088c3b 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 @@ -8,6 +8,9 @@ */ package org.eclipse.hawkbit.repository.jpa; +import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY; +import static org.eclipse.hawkbit.repository.model.Action.Status.DOWNLOADED; +import static org.eclipse.hawkbit.repository.model.Action.Status.FINISHED; import static org.eclipse.hawkbit.repository.model.Target.CONTROLLER_ATTRIBUTE_KEY_SIZE; import static org.eclipse.hawkbit.repository.model.Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE; @@ -452,7 +455,7 @@ public class JpaControllerManagement implements ControllerManagement { "UPDATE sp_target SET last_target_query = #last_target_query WHERE controller_id IN (" + formatQueryInStatementParams(paramMapping.keySet()) + ") AND tenant = #tenant"); - paramMapping.entrySet().forEach(entry -> updateQuery.setParameter(entry.getKey(), entry.getValue())); + paramMapping.forEach(updateQuery::setParameter); updateQuery.setParameter("last_target_query", currentTimeMillis); updateQuery.setParameter("tenant", tenant); @@ -561,23 +564,37 @@ public class JpaControllerManagement implements ControllerManagement { final JpaAction action = getActionAndThrowExceptionIfNotFound(create.getActionId()); final JpaActionStatus actionStatus = create.build(); - // if action is already closed we accept further status updates if - // permitted so by configuration. This is especially useful if the - // action status feedback channel order from the device cannot be - // guaranteed. However, if an action is closed we do not accept further - // close messages. - if (actionIsNotActiveButIntermediateFeedbackStillAllowed(actionStatus, action.isActive())) { - LOG.debug("Update of actionStatus {} for action {} not possible since action not active anymore.", - actionStatus.getStatus(), action.getId()); - return action; + if (isUpdatingActionStatusAllowed(action, actionStatus)) { + return handleAddUpdateActionStatus(actionStatus, action); } - return handleAddUpdateActionStatus(actionStatus, action); + + LOG.debug("Update of actionStatus {} for action {} not possible since action not active anymore.", + actionStatus.getStatus(), action.getId()); + return action; } - private boolean actionIsNotActiveButIntermediateFeedbackStillAllowed(final ActionStatus actionStatus, - final boolean actionActive) { - return !actionActive && (repositoryProperties.isRejectActionStatusForClosedAction() - || Status.ERROR.equals(actionStatus.getStatus()) || Status.FINISHED.equals(actionStatus.getStatus())); + /** + * ActionStatus updates are allowed mainly if the action is active. If the + * action is not active we accept further status updates if permitted so + * by repository configuration. In this case, only the values: Status.ERROR + * and Status.FINISHED are allowed. In the case of a DOWNLOAD_ONLY action, + * we accept status updates only once. + */ + private boolean isUpdatingActionStatusAllowed(final JpaAction action, final JpaActionStatus actionStatus) { + + final boolean isIntermediateFeedback = !FINISHED.equals(actionStatus.getStatus()) + && !Status.ERROR.equals(actionStatus.getStatus()); + + final boolean isAllowedByRepositoryConfiguration = !repositoryProperties.isRejectActionStatusForClosedAction() + && isIntermediateFeedback; + + final boolean isAllowedForDownloadOnlyActions = isDownloadOnly(action) && !isIntermediateFeedback; + + return action.isActive() || isAllowedByRepositoryConfiguration || isAllowedForDownloadOnlyActions; + } + + private static boolean isDownloadOnly(final JpaAction action) { + return DOWNLOAD_ONLY.equals(action.getActionType()); } /** @@ -588,6 +605,10 @@ public class JpaControllerManagement implements ControllerManagement { String controllerId = null; LOG.debug("handleAddUpdateActionStatus for action {}", action.getId()); + // information status entry - check for a potential DOS attack + assertActionStatusQuota(action); + assertActionStatusMessageQuota(actionStatus); + switch (actionStatus.getStatus()) { case ERROR: final JpaTarget target = (JpaTarget) action.getTarget(); @@ -597,10 +618,10 @@ public class JpaControllerManagement implements ControllerManagement { case FINISHED: controllerId = handleFinishedAndStoreInTargetStatus(action); break; + case DOWNLOADED: + controllerId = handleDownloadedActionStatus(action); + break; default: - // information status entry - check for a potential DOS attack - assertActionStatusQuota(action); - assertActionStatusMessageQuota(actionStatus); break; } @@ -615,6 +636,20 @@ public class JpaControllerManagement implements ControllerManagement { return savedAction; } + private String handleDownloadedActionStatus(final JpaAction action) { + if(!isDownloadOnly(action)){ + return null; + } + + JpaTarget target = (JpaTarget) action.getTarget(); + action.setActive(false); + action.setStatus(DOWNLOADED); + target.setUpdateStatus(TargetUpdateStatus.IN_SYNC); + targetRepository.save(target); + + return target.getControllerId(); + } + private void requestControllerAttributes(final String controllerId) { final JpaTarget target = (JpaTarget) getByControllerId(controllerId) .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); @@ -648,6 +683,12 @@ public class JpaControllerManagement implements ControllerManagement { target.setInstalledDistributionSet(ds); target.setInstallationDate(System.currentTimeMillis()); + // Target reported an installation of a DOWNLOAD_ONLY assignment, the assigned DS has to be adapted + // because the currently assigned DS can be unequal to the currently installed DS (the downloadOnly DS) + if(isDownloadOnly(action)){ + target.setAssignedDistributionSet(action.getDistributionSet()); + } + // check if the assigned set is equal to the installed set (not // necessarily the case as another update might be pending already). if (target.getAssignedDistributionSet() != null diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutGroupManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutGroupManagement.java index 0b9bc171e..4ed285608 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutGroupManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutGroupManagement.java @@ -151,7 +151,8 @@ public class JpaRolloutGroupManagement implements RolloutGroupManagement { for (final JpaRolloutGroup rolloutGroup : rolloutGroups) { final TotalTargetCountStatus totalTargetCountStatus = new TotalTargetCountStatus( - allStatesForRollout.get(rolloutGroup.getId()), Long.valueOf(rolloutGroup.getTotalTargets())); + allStatesForRollout.get(rolloutGroup.getId()), Long.valueOf(rolloutGroup.getTotalTargets()), + rolloutGroup.getRollout().getActionType()); rolloutGroup.setTotalTargetCountStatus(totalTargetCountStatus); } @@ -177,7 +178,7 @@ public class JpaRolloutGroupManagement implements RolloutGroupManagement { } final TotalTargetCountStatus totalTargetCountStatus = new TotalTargetCountStatus(rolloutStatusCountItems, - Long.valueOf(jpaRolloutGroup.getTotalTargets())); + Long.valueOf(jpaRolloutGroup.getTotalTargets()), jpaRolloutGroup.getRollout().getActionType()); jpaRolloutGroup.setTotalTargetCountStatus(totalTargetCountStatus); return rolloutGroup; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java index 15e8c5296..064af9c2a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java @@ -105,6 +105,8 @@ import org.springframework.validation.annotation.Validated; import com.google.common.collect.Lists; +import static org.eclipse.hawkbit.repository.jpa.builder.JpaRolloutGroupCreate.addSuccessAndErrorConditionsAndActions; + /** * JPA implementation of {@link RolloutManagement}. */ @@ -126,6 +128,12 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { private static final List ACTIVE_ROLLOUTS = Arrays.asList(RolloutStatus.CREATING, RolloutStatus.DELETING, RolloutStatus.STARTING, RolloutStatus.READY, RolloutStatus.RUNNING); + // In case of DOWNLOAD_ONLY, actions can be finished with DOWNLOADED status. + private static final List DOWNLOAD_ONLY_ACTION_TERMINATION_STATUSES = Arrays.asList(Status.ERROR, + Status.FINISHED, Status.CANCELED, Status.DOWNLOADED); + private static final List DEFAULT_ACTION_TERMINATION_STATUSES = Arrays.asList(Status.ERROR, Status.FINISHED, + Status.CANCELED); + @Autowired private RolloutRepository rolloutRepository; @@ -251,17 +259,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { group.setParent(lastSavedGroup); group.setStatus(RolloutGroupStatus.CREATING); - group.setSuccessCondition(conditions.getSuccessCondition()); - group.setSuccessConditionExp(conditions.getSuccessConditionExp()); - - group.setSuccessAction(conditions.getSuccessAction()); - group.setSuccessActionExp(conditions.getSuccessActionExp()); - - group.setErrorCondition(conditions.getErrorCondition()); - group.setErrorConditionExp(conditions.getErrorConditionExp()); - - group.setErrorAction(conditions.getErrorAction()); - group.setErrorActionExp(conditions.getErrorActionExp()); + addSuccessAndErrorConditionsAndActions(group, conditions); group.setTargetPercentage(1.0F / (amountOfGroups - i) * 100); @@ -310,17 +308,10 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { group.setTargetFilterQuery(""); } - group.setSuccessCondition(srcGroup.getSuccessCondition()); - group.setSuccessConditionExp(srcGroup.getSuccessConditionExp()); - - group.setSuccessAction(srcGroup.getSuccessAction()); - group.setSuccessActionExp(srcGroup.getSuccessActionExp()); - - group.setErrorCondition(srcGroup.getErrorCondition()); - group.setErrorConditionExp(srcGroup.getErrorConditionExp()); - - group.setErrorAction(srcGroup.getErrorAction()); - group.setErrorActionExp(srcGroup.getErrorActionExp()); + addSuccessAndErrorConditionsAndActions(group, srcGroup.getSuccessCondition(), + srcGroup.getSuccessConditionExp(), srcGroup.getSuccessAction(), srcGroup.getSuccessActionExp(), + srcGroup.getErrorCondition(), srcGroup.getErrorConditionExp(), srcGroup.getErrorAction(), + srcGroup.getErrorActionExp()); lastSavedGroup = rolloutGroupRepository.save(group); publishRolloutGroupCreatedEventAfterCommit(lastSavedGroup, rollout); @@ -760,9 +751,11 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { } private boolean isRolloutGroupComplete(final JpaRollout rollout, final JpaRolloutGroup rolloutGroup) { - final Long actionsLeftForRollout = actionRepository - .countByRolloutAndRolloutGroupAndStatusNotAndStatusNotAndStatusNot(rollout, rolloutGroup, - Action.Status.ERROR, Action.Status.FINISHED, Action.Status.CANCELED); + final Long actionsLeftForRollout = ActionType.DOWNLOAD_ONLY.equals(rollout.getActionType()) + ? actionRepository.countByRolloutAndRolloutGroupAndStatusNotIn(rollout, rolloutGroup, + DOWNLOAD_ONLY_ACTION_TERMINATION_STATUSES) + : actionRepository.countByRolloutAndRolloutGroupAndStatusNotIn(rollout, rolloutGroup, + DEFAULT_ACTION_TERMINATION_STATUSES); return actionsLeftForRollout == 0; } @@ -1070,7 +1063,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { } final TotalTargetCountStatus totalTargetCountStatus = new TotalTargetCountStatus(rolloutStatusCountItems, - rollout.get().getTotalTargets()); + rollout.get().getTotalTargets(), rollout.get().getActionType()); ((JpaRollout) rollout.get()).setTotalTargetCountStatus(totalTargetCountStatus); return rollout; } @@ -1112,7 +1105,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { if (allStatesForRollout != null) { rollouts.forEach(rollout -> { final TotalTargetCountStatus totalTargetCountStatus = new TotalTargetCountStatus( - allStatesForRollout.get(rollout.getId()), rollout.getTotalTargets()); + allStatesForRollout.get(rollout.getId()), rollout.getTotalTargets(), rollout.getActionType()); rollout.setTotalTargetCountStatus(totalTargetCountStatus); }); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutGroupCreate.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutGroupCreate.java index 66e7f1610..50c861731 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutGroupCreate.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutGroupCreate.java @@ -11,6 +11,8 @@ package org.eclipse.hawkbit.repository.jpa.builder; import org.eclipse.hawkbit.repository.builder.AbstractRolloutGroupCreate; import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; +import org.eclipse.hawkbit.repository.model.RolloutGroup; +import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; public class JpaRolloutGroupCreate extends AbstractRolloutGroupCreate implements RolloutGroupCreate { @@ -30,20 +32,66 @@ public class JpaRolloutGroupCreate extends AbstractRolloutGroupCreate entry = new SimpleImmutableEntry(action.getTarget().getControllerId(), - action.getId()); - - assertThat(underTest.getActions()).containsExactly(entry); + assertThat(underTest.getActions().size()).isEqualTo(1); + ActionProperties actionProperties = underTest.getActions().get(action.getTarget().getControllerId()); + assertThat(actionProperties).isNotNull(); + assertThat(actionProperties).isEqualToComparingFieldByField(new ActionProperties(action)); assertThat(underTest.getDistributionSetId()).isEqualTo(action.getDistributionSet().getId()); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java index 7cf48859e..cbbac057e 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 @@ -11,6 +11,8 @@ package org.eclipse.hawkbit.repository.jpa; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS; +import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY; +import static org.eclipse.hawkbit.repository.test.util.TestdataFactory.DEFAULT_CONTROLLER_ID; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; @@ -20,6 +22,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -42,19 +45,20 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.ArtifactUpload; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; 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.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; -import org.eclipse.hawkbit.repository.test.util.TestdataFactory; import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -148,7 +152,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, Action.Status.FINISHED, Action.Status.FINISHED, false); assertThat(actionStatusRepository.count()).isEqualTo(7); @@ -199,7 +203,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, Action.Status.FINISHED, Action.Status.FINISHED, false); assertThat(actionStatusRepository.count()).isEqualTo(3); @@ -224,7 +228,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { // expected } - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.RUNNING, true); assertThat(actionStatusRepository.count()).isEqualTo(1); @@ -245,14 +249,14 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.CANCELING, true); simulateIntermediateStatusOnCancellation(actionId); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, Action.Status.CANCELED, Action.Status.FINISHED, false); assertThat(actionStatusRepository.count()).isEqualTo(8); @@ -272,14 +276,14 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.CANCELING, true); simulateIntermediateStatusOnCancellation(actionId); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.CANCELED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, Action.Status.CANCELED, Action.Status.CANCELED, false); assertThat(actionStatusRepository.count()).isEqualTo(8); @@ -300,14 +304,14 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.CANCELING, true); simulateIntermediateStatusOnCancellation(actionId); controllerManagement.addCancelActionStatus( entityFactory.actionStatus().create(actionId).status(Action.Status.CANCEL_REJECTED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.CANCEL_REJECTED, true); assertThat(actionStatusRepository.count()).isEqualTo(8); @@ -328,14 +332,14 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.CANCELING, true); simulateIntermediateStatusOnCancellation(actionId); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.ERROR)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.ERROR, true); assertThat(actionStatusRepository.count()).isEqualTo(8); @@ -346,39 +350,64 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { private Long createTargetAndAssignDs() { final Long dsId = testdataFactory.createDistributionSet().getId(); testdataFactory.createTarget(); - assignDistributionSet(dsId, TestdataFactory.DEFAULT_CONTROLLER_ID); - assertThat(targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getUpdateStatus()) + assignDistributionSet(dsId, DEFAULT_CONTROLLER_ID); + assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getUpdateStatus()) .isEqualTo(TargetUpdateStatus.PENDING); - return deploymentManagement.findActiveActionsByTarget(PAGE, TestdataFactory.DEFAULT_CONTROLLER_ID).getContent() + return deploymentManagement.findActiveActionsByTarget(PAGE, DEFAULT_CONTROLLER_ID).getContent() .get(0).getId(); } + @Step + private Long createAndAssignDsAsDownloadOnly(final String dsName, final String defaultControllerId) { + final Long dsId = testdataFactory.createDistributionSet(dsName).getId(); + assignDistributionSet(dsId, defaultControllerId, DOWNLOAD_ONLY); + assertThat(targetManagement.getByControllerID(defaultControllerId).get().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + + final Long id = deploymentManagement.findActiveActionsByTarget(PAGE, defaultControllerId).getContent() + .get(0).getId(); + assertThat(id).isNotNull(); + return id; + } + + @Step + private Long assignDs(final Long dsId, final String defaultControllerId, final Action.ActionType actionType) { + assignDistributionSet(dsId, defaultControllerId, actionType); + assertThat(targetManagement.getByControllerID(defaultControllerId).get().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + + final Long id = deploymentManagement.findActiveActionsByTarget(PAGE, defaultControllerId).getContent() + .get(0).getId(); + assertThat(id).isNotNull(); + return id; + } + @Step private void simulateIntermediateStatusOnCancellation(final Long actionId) { controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.RUNNING)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.RUNNING, true); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.DOWNLOAD)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.DOWNLOAD, true); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.DOWNLOADED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.DOWNLOADED, true); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.RETRIEVED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.RETRIEVED, true); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.WARNING)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.WARNING, true); } @@ -386,26 +415,26 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { private void simulateIntermediateStatusOnUpdate(final Long actionId) { controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.RUNNING)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.RUNNING, true); controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.DOWNLOAD)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.DOWNLOAD, true); controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.DOWNLOADED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.DOWNLOADED, true); controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.RETRIEVED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.RETRIEVED, true); controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.WARNING)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.WARNING, true); } @@ -509,7 +538,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Controller trys to finish an update process after it has been finished by an error action status.") + @Description("Controller tries to finish an update process after it has been finished by an error action status.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1), @@ -522,12 +551,12 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { // test and verify controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.RUNNING)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.RUNNING, true); controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.ERROR)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.ERROR, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.ERROR, Action.Status.ERROR, Action.Status.ERROR, false); // try with disabled late feedback @@ -536,7 +565,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); // test - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.ERROR, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.ERROR, Action.Status.ERROR, Action.Status.ERROR, false); // try with enabled late feedback - should not make a difference as it @@ -546,7 +575,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); // test - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.ERROR, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.ERROR, Action.Status.ERROR, Action.Status.ERROR, false); assertThat(actionStatusRepository.count()).isEqualTo(3); @@ -572,7 +601,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); // test - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, Action.Status.FINISHED, Action.Status.FINISHED, false); // try with enabled late feedback - should not make a difference as it @@ -582,7 +611,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); // test - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, Action.Status.FINISHED, Action.Status.FINISHED, false); assertThat(actionStatusRepository.count()).isEqualTo(3); @@ -609,7 +638,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { entityFactory.actionStatus().create(action.getId()).status(Action.Status.RUNNING)); // nothing changed as "feedback after close" is disabled - assertThat(targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getUpdateStatus()) + assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getUpdateStatus()) .isEqualTo(TargetUpdateStatus.IN_SYNC); assertThat(actionStatusRepository.count()).isEqualTo(3); @@ -635,7 +664,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { entityFactory.actionStatus().create(action.getId()).status(Action.Status.RUNNING)); // nothing changed as "feedback after close" is disabled - assertThat(targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getUpdateStatus()) + assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getUpdateStatus()) .isEqualTo(TargetUpdateStatus.IN_SYNC); // however, additional action status has been stored @@ -989,4 +1018,312 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } + @Test + @Description("Verifies that a DOWNLOAD_ONLY action is not marked complete when the controller reports DOWNLOAD") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void controllerReportsDownloadForDownloadOnlyAction() { + testdataFactory.createTarget(); + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOAD)); + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + Action.Status.RUNNING, Action.Status.DOWNLOAD, true); + + assertThat(actionStatusRepository.count()).isEqualTo(2); + assertThat(controllerManagement.findActionStatusByAction(PAGE, actionId).getNumberOfElements()).isEqualTo(2); + assertThat(actionRepository.activeActionExistsForControllerId(DEFAULT_CONTROLLER_ID)) + .isEqualTo(true); + } + + @Test + @Description("Verifies that a DOWNLOAD_ONLY action is marked complete once the controller reports DOWNLOADED") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = TargetAttributesRequestedEvent.class, count = 1), + @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void controllerReportsDownloadedForDownloadOnlyAction() { + testdataFactory.createTarget(); + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED)); + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + Action.Status.DOWNLOADED, Action.Status.DOWNLOADED, false); + + assertThat(actionStatusRepository.count()).isEqualTo(2); + assertThat(controllerManagement.findActionStatusByAction(PAGE, actionId).getNumberOfElements()).isEqualTo(2); + assertThat(actionRepository.activeActionExistsForControllerId(DEFAULT_CONTROLLER_ID)) + .isEqualTo(false); + } + + @Test + @Description("Verifies that a controller can report a FINISHED event for a DOWNLOAD_ONLY non-active action.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 3), + @Expect(type = TargetAttributesRequestedEvent.class, count = 2), + @Expect(type = ActionUpdatedEvent.class, count = 2), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void controllerReportsActionFinishedForDownloadOnlyActionThatIsNotActive() { + testdataFactory.createTarget(); + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + finishDownloadOnlyUpdateAndSendUpdateActionStatus(actionId, Status.FINISHED); + + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + Action.Status.FINISHED, Action.Status.FINISHED, false); + + assertThat(actionStatusRepository.count()).isEqualTo(3); + assertThat(controllerManagement.findActionStatusByAction(PAGE, actionId).getNumberOfElements()).isEqualTo(3); + assertThat(actionRepository.activeActionExistsForControllerId(DEFAULT_CONTROLLER_ID)) + .isEqualTo(false); + } + + @Test + @Description("Verifies that multiple DOWNLOADED events for a DOWNLOAD_ONLY action are handled.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = TargetAttributesRequestedEvent.class, count = 3), + @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void controllerReportsMultipleDownloadedForDownloadOnlyAction() { + testdataFactory.createTarget(); + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + IntStream.range(0, 3).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED))); + + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + Status.DOWNLOADED, Status.DOWNLOADED, false); + + assertThat(actionStatusRepository.count()).isEqualTo(4); + assertThat(controllerManagement.findActionStatusByAction(PAGE, actionId).getNumberOfElements()).isEqualTo(4); + assertThat(actionRepository.activeActionExistsForControllerId(DEFAULT_CONTROLLER_ID)) + .isEqualTo(false); + } + + @Test(expected = QuotaExceededException.class) + @Description("Verifies that quota is asserted when a controller reports too many DOWNLOADED events for a " + + "DOWNLOAD_ONLY action.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = TargetAttributesRequestedEvent.class, count = 9), + @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void quotaExceptionWhencontrollerReportsTooManyDownloadedMessagesForDownloadOnlyAction() { + final int maxMessages = quotaManagement.getMaxMessagesPerActionStatus(); + testdataFactory.createTarget(); + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + + IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED))); + } + + @Test + @Description("Verifies that quota is enforced for UpdateActionStatus events for DOWNLOAD_ONLY assignments.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = TargetAttributesRequestedEvent.class, count = 9), + @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void quotaEceededExceptionWhenControllerReportsTooManyUpdateActionStatusMessagesForDownloadOnlyAction() { + final int maxMessages = quotaManagement.getMaxMessagesPerActionStatus(); + testdataFactory.createTarget(); + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + + try { + 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) { } + + 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) { } + + 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) { } + } + + @Test + @Description("Verifies that quota is enforced for UpdateActionStatus events for FORCED assignments.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void quotaEceededExceptionWhenControllerReportsTooManyUpdateActionStatusMessagesForForced() { + final int maxMessages = quotaManagement.getMaxMessagesPerActionStatus(); + final Long actionId = createTargetAndAssignDs(); + assertThat(actionId).isNotNull(); + + try { + 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) { } + + 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) { } + + 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) { } + } + + @Test + @Description("Verifies that a target can report FINISHED/ERROR updates for DOWNLOAD_ONLY assignments regardless of " + + "repositoryProperties.rejectActionStatusForClosedAction value.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 4), + @Expect(type = ActionCreatedEvent.class, count = 4), + @Expect(type = TargetUpdatedEvent.class, count = 12), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 4), + @Expect(type = TargetAttributesRequestedEvent.class, count = 6), + @Expect(type = ActionUpdatedEvent.class, count = 8), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 12) }) + public void targetCanAlwaysReportFinishedOrErrorAfterActionIsClosedForDownloadOnlyAssignments() { + + testdataFactory.createTarget(); + + // allow actionStatusUpdates for closed actions + repositoryProperties.setRejectActionStatusForClosedAction(false); + + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs1", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + finishDownloadOnlyUpdateAndSendUpdateActionStatus(actionId, Status.FINISHED); + + final Long actionId2 = createAndAssignDsAsDownloadOnly("downloadOnlyDs2", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + finishDownloadOnlyUpdateAndSendUpdateActionStatus(actionId2, Status.ERROR); + + // disallow actionStatusUpdates for closed actions + repositoryProperties.setRejectActionStatusForClosedAction(true); + + final Long actionId3 = createAndAssignDsAsDownloadOnly("downloadOnlyDs3", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + finishDownloadOnlyUpdateAndSendUpdateActionStatus(actionId3, Status.FINISHED); + + + final Long actionId4 = createAndAssignDsAsDownloadOnly("downloadOnlyDs4", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + finishDownloadOnlyUpdateAndSendUpdateActionStatus(actionId4, Status.ERROR); + + // actionStatusRepository should have 12 ActionStatusUpdates, 3 from each action + assertThat(actionStatusRepository.count()).isEqualTo(12L); + } + + @Step + private void finishDownloadOnlyUpdateAndSendUpdateActionStatus(final Long actionId, final Status status) { + // finishing action + controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId) + .status(Status.DOWNLOADED)); + + controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId) + .status(status)); + assertThat(actionRepository.activeActionExistsForControllerId(DEFAULT_CONTROLLER_ID)) + .isEqualTo(false); + } + + @Test + @Description("Verifies that a controller can report a FINISHED event for a DOWNLOAD_ONLY action after having" + + " installed an intermediate update.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 2), + @Expect(type = ActionCreatedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 5), + @Expect(type = TargetAttributesRequestedEvent.class, count = 3), + @Expect(type = ActionUpdatedEvent.class, count = 3), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 2), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6) }) + public void controllerReportsFinishedForOldDownloadOnlyActionAfterSuccessfulForcedAssignment() { + + testdataFactory.createTarget(); + final DistributionSet downloadOnlyDs = testdataFactory.createDistributionSet("downloadOnlyDs1"); + + // assign DOWNLOAD_ONLY Distribution set + final Long downloadOnlyActionId = assignDs(downloadOnlyDs.getId(), DEFAULT_CONTROLLER_ID, DOWNLOAD_ONLY); + addUpdateActionStatus(downloadOnlyActionId, DEFAULT_CONTROLLER_ID, Status.DOWNLOADED); + assertAssignedDistributionSetId(DEFAULT_CONTROLLER_ID, downloadOnlyDs.getId()); + assertInstalledDistributionSetId(DEFAULT_CONTROLLER_ID, null); + assertNoActiveActionsExistsForControllerId(DEFAULT_CONTROLLER_ID); + + // assign distributionSet as FORCED assignment + 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); + assertAssignedDistributionSetId(DEFAULT_CONTROLLER_ID, forcedDistributionSetId); + assertInstalledDistributionSetId(DEFAULT_CONTROLLER_ID, forcedDistributionSetId); + assertNoActiveActionsExistsForControllerId(DEFAULT_CONTROLLER_ID); + + // report FINISHED for the DOWNLOAD_ONLY action + addUpdateActionStatus(downloadOnlyActionId, DEFAULT_CONTROLLER_ID, Status.FINISHED); + assertAssignedDistributionSetId(DEFAULT_CONTROLLER_ID, downloadOnlyDs.getId()); + assertInstalledDistributionSetId(DEFAULT_CONTROLLER_ID, downloadOnlyDs.getId()); + assertNoActiveActionsExistsForControllerId(DEFAULT_CONTROLLER_ID); + } + + @Step + private void addUpdateActionStatus(final Long actionId, final String controllerId, final Status actionStatus) { + controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(actionStatus)); + assertActionStatus(actionId, controllerId, TargetUpdateStatus.IN_SYNC, actionStatus, actionStatus, false); + } + + private void assertAssignedDistributionSetId(final String controllerId, final Long dsId) { + final Optional target = controllerManagement.getByControllerId(controllerId); + assertThat(target).isPresent(); + final DistributionSet assignedDistributionSet = ((JpaTarget) target.get()).getAssignedDistributionSet(); + assertThat(assignedDistributionSet.getId()).isEqualTo(dsId); + } + + private void assertInstalledDistributionSetId(final String controllerId, final Long dsId) { + final Optional target = controllerManagement.getByControllerId(controllerId); + assertThat(target).isPresent(); + final DistributionSet installedDistributionSet = ((JpaTarget) target.get()).getInstalledDistributionSet(); + if(dsId == null){ + assertThat(installedDistributionSet).isNull(); + } else { + assertThat(installedDistributionSet.getId()).isEqualTo(dsId); + } + } + + private void assertNoActiveActionsExistsForControllerId(final String controllerId) { + assertThat(actionRepository.activeActionExistsForControllerId(controllerId)).isEqualTo(false); + } + } 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 ddb336406..58de216b4 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 @@ -47,6 +47,7 @@ import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionProperties; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; @@ -1086,11 +1087,15 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { assertThat(event).isNotNull(); assertThat(event.getDistributionSetId()).isEqualTo(ds.getId()); - assertThat(event.getActions()).isEqualTo(targets.stream() - .map(target -> deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId()) - .getContent()) + List eventActionIds = event.getActions().values().stream().map(ActionProperties::getId) + .collect(Collectors.toList()); + + List targetActiveActionIds = targets.stream() + .map(t -> deploymentManagement.findActiveActionsByTarget(PAGE, t.getControllerId()).getContent()) .flatMap(List::stream) - .collect(Collectors.toMap(action -> action.getTarget().getControllerId(), Action::getId))); + .map(Action::getId) + .collect(Collectors.toList()); + assertThat(eventActionIds).containsOnlyElementsOf(targetActiveActionIds); } private class DeploymentResult { 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 1e91f5051..7eb116df3 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 @@ -590,6 +590,69 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { } + @Test + @Description("Verify that the targets have the right status during a download_only rollout.") + public void countCorrectStatusForEachTargetDuringDownloadOnlyRollout() { + + final int amountTargetsForRollout = 8; + final int amountOtherTargets = 15; + final int amountGroups = 4; + final String successCondition = "50"; + final String errorCondition = "80"; + final Rollout createdRollout = createSimpleTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout, + amountOtherTargets, amountGroups, successCondition, errorCondition, ActionType.DOWNLOAD_ONLY); + + // targets have not started + Map validationMap = createInitStatusMap(); + validationMap.put(TotalTargetCountStatus.Status.NOTSTARTED, 8L); + validateRolloutActionStatus(createdRollout.getId(), validationMap); + + rolloutManagement.start(createdRollout.getId()); + + // Run here, because scheduler is disabled during tests + rolloutManagement.handleRollouts(); + + // 6 targets are ready and 2 are running + validationMap = createInitStatusMap(); + validationMap.put(TotalTargetCountStatus.Status.SCHEDULED, 6L); + validationMap.put(TotalTargetCountStatus.Status.RUNNING, 2L); + validateRolloutActionStatus(createdRollout.getId(), validationMap); + + changeStatusForAllRunningActions(createdRollout, Status.DOWNLOADED); + rolloutManagement.handleRollouts(); + // 4 targets are ready, 2 are finished(with DOWNLOADED action status) and 2 are running + validationMap = createInitStatusMap(); + validationMap.put(TotalTargetCountStatus.Status.SCHEDULED, 4L); + validationMap.put(TotalTargetCountStatus.Status.FINISHED, 2L); + validationMap.put(TotalTargetCountStatus.Status.RUNNING, 2L); + validateRolloutActionStatus(createdRollout.getId(), validationMap); + + changeStatusForAllRunningActions(createdRollout, Status.DOWNLOADED); + rolloutManagement.handleRollouts(); + // 2 targets are ready, 4 are finished(with DOWNLOADED action status) and 2 are running + validationMap = createInitStatusMap(); + validationMap.put(TotalTargetCountStatus.Status.SCHEDULED, 2L); + validationMap.put(TotalTargetCountStatus.Status.FINISHED, 4L); + validationMap.put(TotalTargetCountStatus.Status.RUNNING, 2L); + validateRolloutActionStatus(createdRollout.getId(), validationMap); + + changeStatusForAllRunningActions(createdRollout, Status.DOWNLOADED); + rolloutManagement.handleRollouts(); + // 0 targets are ready, 6 are finished(with DOWNLOADED action status) and 2 are running + validationMap = createInitStatusMap(); + validationMap.put(TotalTargetCountStatus.Status.FINISHED, 6L); + validationMap.put(TotalTargetCountStatus.Status.RUNNING, 2L); + validateRolloutActionStatus(createdRollout.getId(), validationMap); + + changeStatusForAllRunningActions(createdRollout, Status.FINISHED); + rolloutManagement.handleRollouts(); + // 0 targets are ready, 6 are finished(with DOWNLOADED action status), 2 are finished and 0 are running + validationMap = createInitStatusMap(); + validationMap.put(TotalTargetCountStatus.Status.FINISHED, 8L); + validateRolloutActionStatus(createdRollout.getId(), validationMap); + + } + @Test @Description("Verify that the targets have the right status during the rollout when an error emerges.") public void countCorrectStatusForEachTargetDuringRolloutWithError() { @@ -1727,12 +1790,19 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { private Rollout createSimpleTestRolloutWithTargetsAndDistributionSet(final int amountTargetsForRollout, final int amountOtherTargets, final int groupSize, final String successCondition, final String errorCondition) { + return createSimpleTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout, amountOtherTargets, + groupSize, successCondition, errorCondition, ActionType.FORCED); + } + + private Rollout createSimpleTestRolloutWithTargetsAndDistributionSet(final int amountTargetsForRollout, + final int amountOtherTargets, final int groupSize, final String successCondition, + final String errorCondition, final ActionType actionType) { final DistributionSet rolloutDS = testdataFactory.createDistributionSet("rolloutDS"); testdataFactory.createTargets(amountTargetsForRollout, "rollout-", "rollout"); testdataFactory.createTargets(amountOtherTargets, "others-", "rollout"); final String filterQuery = "controllerId==rollout-*"; return testdataFactory.createRolloutByVariables("test-rollout-name-1", "test-rollout-description-1", groupSize, - filterQuery, rolloutDS, successCondition, errorCondition); + filterQuery, rolloutDS, successCondition, errorCondition, actionType); } private Rollout createTestRolloutWithTargetsAndDistributionSet(final int amountTargetsForRollout, diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java index b50362434..6ee867a96 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java @@ -196,6 +196,8 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest verifyAutoAssignmentWithSoftActionType(filterName, targetFilterQuery, distributionSet); + verifyAutoAssignmentWithDownloadOnlyActionType(filterName, targetFilterQuery, distributionSet); + verifyAutoAssignmentWithInvalidActionType(targetFilterQuery, distributionSet); verifyAutoAssignmentWithIncompleteDs(targetFilterQuery); @@ -218,6 +220,14 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest verifyAutoAssignDsAndActionType(filterName, distributionSet, ActionType.SOFT); } + @Step + private void verifyAutoAssignmentWithDownloadOnlyActionType(final String filterName, + final TargetFilterQuery targetFilterQuery, final DistributionSet distributionSet) { + targetFilterQueryManagement.updateAutoAssignDSWithActionType(targetFilterQuery.getId(), distributionSet.getId(), + ActionType.DOWNLOAD_ONLY); + verifyAutoAssignDsAndActionType(filterName, distributionSet, ActionType.DOWNLOAD_ONLY); + } + @Step private void verifyAutoAssignmentWithInvalidActionType(final TargetFilterQuery targetFilterQuery, final DistributionSet distributionSet) { 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 bfa9885ec..ae230a24f 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 @@ -211,36 +211,46 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest { } @Test - @Description("Test auto assignment of a distribution set with FORCED and SOFT action types") + @Description("Test auto assignment of a distribution set with FORCED, SOFT and DOWNLOAD_ONLY action types") public void checkAutoAssignWithDifferentActionTypes() { final DistributionSet distributionSet = testdataFactory.createDistributionSet(); - final String targetDsAIdPref = "targA"; - final String targetDsBIdPref = "targB"; + final String targetDsAIdPref = "A"; + final String targetDsBIdPref = "B"; + final String targetDsCIdPref = "C"; - final List targetsA = testdataFactory.createTargets(5, targetDsAIdPref, - targetDsAIdPref.concat(" description")); - final List targetsB = testdataFactory.createTargets(10, targetDsBIdPref, - targetDsBIdPref.concat(" description")); - final int targetsCount = targetsA.size() + targetsB.size(); + List targetsA = createTargetsAndAutoAssignDistSet(targetDsAIdPref, 5, distributionSet, + ActionType.FORCED); + List targetsB = createTargetsAndAutoAssignDistSet(targetDsBIdPref, 10, distributionSet, + ActionType.SOFT); + List targetsC = createTargetsAndAutoAssignDistSet(targetDsCIdPref, 10, distributionSet, + ActionType.DOWNLOAD_ONLY); - targetFilterQueryManagement - .updateAutoAssignDSWithActionType( - targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("filterA") - .query("id==" + targetDsAIdPref + "*")).getId(), - distributionSet.getId(), ActionType.FORCED); - targetFilterQueryManagement - .updateAutoAssignDSWithActionType( - targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("filterB") - .query("id==" + targetDsBIdPref + "*")).getId(), - distributionSet.getId(), ActionType.SOFT); + final int targetsCount = targetsA.size() + targetsB.size() + targetsC.size(); autoAssignChecker.check(); verifyThatTargetsHaveDistributionSetAssignment(distributionSet, targetsA, targetsCount); verifyThatTargetsHaveDistributionSetAssignment(distributionSet, targetsB, targetsCount); + verifyThatTargetsHaveDistributionSetAssignment(distributionSet, targetsC, targetsCount); verifyThatTargetsHaveAssignmentActionType(ActionType.FORCED, targetsA); verifyThatTargetsHaveAssignmentActionType(ActionType.SOFT, targetsB); + verifyThatTargetsHaveAssignmentActionType(ActionType.DOWNLOAD_ONLY, targetsC); + } + + @Step + private List createTargetsAndAutoAssignDistSet(final String prefix, final int targetCount, + final DistributionSet distributionSet, final ActionType actionType) { + + final List targets = testdataFactory.createTargets(targetCount, "target" + prefix, + prefix.concat(" description")); + + targetFilterQueryManagement + .updateAutoAssignDSWithActionType( + targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create() + .name("filter" + prefix).query("id==target" + prefix + "*")).getId(), + distributionSet.getId(), actionType); + return targets; } @Step diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java index 7c707099b..ed4146458 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java @@ -225,8 +225,14 @@ public abstract class AbstractIntegrationTest { }; protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final String controllerId) { - return deploymentManagement.assignDistributionSet(dsID, Arrays.asList( - new TargetWithActionType(controllerId, ActionType.FORCED, RepositoryModelConstants.NO_FORCE_TIME))); + return assignDistributionSet(dsID, controllerId, ActionType.FORCED); + } + + protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final String controllerId, + final ActionType actionType) { + return deploymentManagement.assignDistributionSet(dsID, + Collections.singletonList(new TargetWithActionType(controllerId, actionType, + RepositoryModelConstants.NO_FORCE_TIME))); } /** @@ -316,8 +322,8 @@ public abstract class AbstractIntegrationTest { final boolean isRequiredMigrationStep) { final DistributionSet ds = testdataFactory.createDistributionSet(distributionSet, isRequiredMigrationStep); Target savedTarget = testdataFactory.createTarget(controllerId); - savedTarget = assignDistributionSet(ds.getId(), savedTarget.getControllerId()).getAssignedEntity().iterator() - .next(); + savedTarget = assignDistributionSet(ds.getId(), savedTarget.getControllerId(), ActionType.FORCED) + .getAssignedEntity().iterator().next(); Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) .getContent().get(0); diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java index 5dcf1f9ee..7f38983dc 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java @@ -1003,14 +1003,41 @@ public class TestdataFactory { public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription, final int groupSize, final String filterQuery, final DistributionSet distributionSet, final String successCondition, final String errorCondition) { + return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet, successCondition, errorCondition, Action.ActionType.FORCED); + } + + /** + * Creates rollout based on given parameters. + * + * @param rolloutName + * of the {@link Rollout} + * @param rolloutDescription + * of the {@link Rollout} + * @param groupSize + * of the {@link Rollout} + * @param filterQuery + * to identify the {@link Target}s + * @param distributionSet + * to assign + * @param successCondition + * to switch to next group + * @param errorCondition + * to switch to next group + * @param actionType + * the type of the Rollout + * @return created {@link Rollout} + */ + public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription, + final int groupSize, final String filterQuery, final DistributionSet distributionSet, + final String successCondition, final String errorCondition, final Action.ActionType actionType) { final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults() .successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition) .errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition) .errorAction(RolloutGroupErrorAction.PAUSE, null).build(); final Rollout rollout = rolloutManagement.create(entityFactory.rollout().create().name(rolloutName) - .description(rolloutDescription).targetFilterQuery(filterQuery).set(distributionSet), groupSize, - conditions); + .description(rolloutDescription).targetFilterQuery(filterQuery).set(distributionSet) + .actionType(actionType), groupSize, conditions); // Run here, because Scheduler is disabled during tests rolloutManagement.handleRollouts(); diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java index fc6158593..df9977ffc 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java @@ -57,8 +57,8 @@ public class MgmtAction extends MgmtBaseEntity { @JsonProperty private Long forceTime; - @JsonProperty - private MgmtActionType forceType; + @JsonProperty(value="forceType") + private MgmtActionType actionType; @JsonProperty private MgmtMaintenanceWindow maintenanceWindow; @@ -79,12 +79,12 @@ public class MgmtAction extends MgmtBaseEntity { this.forceTime = forceTime; } - public MgmtActionType getForceType() { - return forceType; + public MgmtActionType getActionType() { + return actionType; } - public void setForceType(final MgmtActionType forceType) { - this.forceType = forceType; + public void setActionType(final MgmtActionType actionType) { + this.actionType = actionType; } public String getStatus() { diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtActionRequestBodyPut.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtActionRequestBodyPut.java index 0497481cf..d7b7ee8fd 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtActionRequestBodyPut.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtActionRequestBodyPut.java @@ -18,15 +18,15 @@ import com.fasterxml.jackson.annotation.JsonProperty; */ public class MgmtActionRequestBodyPut { - @JsonProperty - private MgmtActionType forceType; + @JsonProperty(value="forceType") + private MgmtActionType actionType; - public MgmtActionType getForceType() { - return forceType; + public MgmtActionType getActionType() { + return actionType; } - public void setForceType(final MgmtActionType forceType) { - this.forceType = forceType; + public void setActionType(final MgmtActionType actionType) { + this.actionType = actionType; } } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtActionType.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtActionType.java index d95375f5a..0601b954a 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtActionType.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtActionType.java @@ -28,7 +28,12 @@ public enum MgmtActionType { /** * The time forced action type. */ - TIMEFORCED("timeforced"); + TIMEFORCED("timeforced"), + + /** + * The Download-Only action type. + */ + DOWNLOAD_ONLY("downloadonly"); private final String name; diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java index 3a32e060a..454bfc994 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; /** * @@ -43,6 +44,9 @@ public class MgmtRolloutResponseBody extends MgmtNamedEntity { @JsonProperty private boolean deleted; + @JsonProperty + private MgmtActionType type; + public boolean isDeleted() { return deleted; } @@ -102,4 +106,12 @@ public class MgmtRolloutResponseBody extends MgmtNamedEntity { totalTargetsPerStatus.put(status, totalTargetCountByStatus); } + + public void setType(final MgmtActionType type) { + this.type = type; + } + + public MgmtActionType getType() { + return type; + } } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/tag/MgmtAssignedDistributionSetRequestBody.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/tag/MgmtAssignedDistributionSetRequestBody.java index 469780abf..4c004b206 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/tag/MgmtAssignedDistributionSetRequestBody.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/tag/MgmtAssignedDistributionSetRequestBody.java @@ -28,9 +28,8 @@ public class MgmtAssignedDistributionSetRequestBody { return distributionSetId; } - public MgmtAssignedDistributionSetRequestBody setDistributionSetId(final Long distributionSetId) { + public void setDistributionSetId(final Long distributionSetId) { this.distributionSetId = distributionSetId; - return this; } } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java index 95137b1ba..dc5a46411 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java @@ -20,7 +20,7 @@ import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity; * back. * */ -final class MgmtRestModelMapper { +public final class MgmtRestModelMapper { // private constructor, utility class private MgmtRestModelMapper() { @@ -66,6 +66,8 @@ final class MgmtRestModelMapper { return ActionType.FORCED; case TIMEFORCED: return ActionType.TIMEFORCED; + case DOWNLOAD_ONLY: + return ActionType.DOWNLOAD_ONLY; default: throw new IllegalStateException("Action Type is not supported"); } @@ -92,6 +94,8 @@ final class MgmtRestModelMapper { return MgmtActionType.FORCED; case TIMEFORCED: return MgmtActionType.TIMEFORCED; + case DOWNLOAD_ONLY: + return MgmtActionType.DOWNLOAD_ONLY; default: throw new IllegalStateException("Action Type is not supported"); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java index 87638e58f..f380c106f 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java @@ -78,6 +78,7 @@ final class MgmtRolloutMapper { body.setStatus(rollout.getStatus().toString().toLowerCase()); body.setTotalTargets(rollout.getTotalTargets()); body.setDeleted(rollout.isDeleted()); + body.setType(MgmtRestModelMapper.convertActionType(rollout.getActionType())); if (withDetails) { for (final TotalTargetCountStatus.Status status : TotalTargetCountStatus.Status.values()) { diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java index 782084361..b34f62967 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java @@ -205,7 +205,7 @@ public final class MgmtTargetMapper { if (ActionType.TIMEFORCED.equals(action.getActionType())) { result.setForceTime(action.getForcedTime()); } - result.setForceType(MgmtRestModelMapper.convertActionType(action.getActionType())); + result.setActionType(MgmtRestModelMapper.convertActionType(action.getActionType())); if (action.isActive()) { result.setStatus(MgmtAction.ACTION_PENDING); diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java index 9e64d84e1..7e6790b60 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java @@ -346,7 +346,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi { return ResponseEntity.notFound().build(); } - if (!MgmtActionType.FORCED.equals(actionUpdate.getForceType())) { + if (!MgmtActionType.FORCED.equals(actionUpdate.getActionType())) { throw new ValidationException("Resource supports only switch to FORCED."); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java index d06121c1a..740d461bd 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java @@ -33,6 +33,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; +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.DistributionSetMetadata; @@ -1245,4 +1246,30 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr return created; } + @Test + @Description("Ensures that multi target assignment through API is reflected by the repository in the case of " + + "DOWNLOAD_ONLY.") + public void assignMultipleTargetsToDistributionSetAsDownloadOnly() throws Exception { + final DistributionSet createdDs = testdataFactory.createDistributionSet(); + + // prepare targets + final String[] knownTargetIds = new String[] { "1", "2", "3", "4", "5" }; + final JSONArray list = new JSONArray(); + for (final String targetId : knownTargetIds) { + testdataFactory.createTarget(targetId); + list.put(new JSONObject().put("id", Long.valueOf(targetId))); + } + // assign already one target to DS + assignDistributionSet(createdDs.getId(), knownTargetIds[0], Action.ActionType.DOWNLOAD_ONLY); + + mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", createdDs.getId()) + .contentType(MediaType.APPLICATION_JSON).content(list.toString())) + .andExpect(status().isOk()).andExpect(jsonPath("$.assigned", equalTo(knownTargetIds.length - 1))) + .andExpect(jsonPath("$.alreadyAssigned", equalTo(1))) + .andExpect(jsonPath("$.total", equalTo(knownTargetIds.length))); + + assertThat(targetManagement.findByAssignedDistributionSet(PAGE, createdDs.getId()).getContent()) + .as("Five targets in repository have DS assigned").hasSize(5); + } + } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java index 13a84c44a..7835531e8 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java @@ -32,6 +32,7 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; @@ -130,7 +131,7 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes testdataFactory.createTargets(20, "target", "rollout"); final DistributionSet dsA = testdataFactory.createDistributionSet(""); - postRollout("rollout1", 10, dsA.getId(), "id==target*", 20); + postRollout("rollout1", 10, dsA.getId(), "id==target*", 20, Action.ActionType.FORCED); } @Test @@ -367,8 +368,8 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes testdataFactory.createTargets(20, "target", "rollout"); // setup - create 2 rollouts - postRollout("rollout1", 10, dsA.getId(), "id==target*", 20); - postRollout("rollout2", 5, dsA.getId(), "id==target-0001*", 10); + postRollout("rollout1", 10, dsA.getId(), "id==target*", 20, Action.ActionType.FORCED); + postRollout("rollout2", 5, dsA.getId(), "id==target-0001*", 10, Action.ActionType.FORCED); // Run here, because Scheduler is disabled during tests rolloutManagement.handleRollouts(); @@ -408,8 +409,8 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes testdataFactory.createTargets(20, "target", "rollout"); // setup - create 2 rollouts - postRollout("rollout1", 10, dsA.getId(), "id==target*", 20); - postRollout("rollout2", 5, dsA.getId(), "id==target*", 20); + postRollout("rollout1", 10, dsA.getId(), "id==target*", 20, Action.ActionType.FORCED); + postRollout("rollout2", 5, dsA.getId(), "id==target*", 20, Action.ActionType.FORCED); // Run here, because Scheduler is disabled during tests rolloutManagement.handleRollouts(); @@ -906,6 +907,15 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes } + @Test + @Description("Verifies that a DOWNLOAD_ONLY rollout is possible") + public void createDownloadOnlyRollout() throws Exception { + testdataFactory.createTargets(20, "target", "rollout"); + + final DistributionSet dsA = testdataFactory.createDistributionSet(""); + postRollout("rollout1", 10, dsA.getId(), "id==target*", 20, Action.ActionType.DOWNLOAD_ONLY); + } + protected T doWithTimeout(final Callable callable, final SuccessCondition successCondition, final long timeout, final long pollInterval) throws Exception // NOPMD { @@ -944,13 +954,17 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes } private void postRollout(final String name, final int groupSize, final Long distributionSetId, - final String targetFilterQuery, final int targets) throws Exception { + final String targetFilterQuery, final int targets, final Action.ActionType type) throws Exception { + String actionType = MgmtRestModelMapper.convertActionType(type).getName(); + String rollout = JsonBuilder.rollout(name, "desc", groupSize, distributionSetId, targetFilterQuery, + new RolloutGroupConditionBuilder().withDefaults().build(), null, actionType); + mvc.perform(post("/rest/v1/rollouts") - .content(JsonBuilder.rollout(name, "desc", groupSize, distributionSetId, targetFilterQuery, - new RolloutGroupConditionBuilder().withDefaults().build())) + .content(rollout) .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) .andExpect(jsonPath("$.name", equalTo(name))).andExpect(jsonPath("$.status", equalTo("creating"))) + .andExpect(jsonPath("$.type", equalTo(actionType))) .andExpect(jsonPath("$.targetFilterQuery", equalTo(targetFilterQuery))) .andExpect(jsonPath("$.description", equalTo("desc"))) .andExpect(jsonPath("$.distributionSetId", equalTo(distributionSetId.intValue()))) diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java index da2523ec9..fdeadcbd1 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.mgmt.rest.resource; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.rest.util.MockMvcResultPrinter.print; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasSize; @@ -104,7 +105,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte public void updateTargetWhichDoesNotExistsLeadsToEntityNotFound() throws Exception { final String notExistingId = "4395"; mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + notExistingId).content("{}") - .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .contentType(MediaType.APPLICATION_JSON)).andDo(print()) .andExpect(status().isNotFound()); } @@ -120,7 +121,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte final TargetFilterQuery tfq = createSingleTargetFilterQuery(filterName, filterQuery); mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId()).content(body) - .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .contentType(MediaType.APPLICATION_JSON)).andDo(print()).andExpect(status().isOk()) .andExpect(jsonPath(JSON_PATH_ID, equalTo(tfq.getId().intValue()))) .andExpect(jsonPath(JSON_PATH_QUERY, equalTo(filterQuery2))) .andExpect(jsonPath(JSON_PATH_NAME, equalTo(filterName))); @@ -143,7 +144,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte .create(entityFactory.targetFilterQuery().create().name(filterName).query(filterQuery)); mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId()).content(body) - .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .contentType(MediaType.APPLICATION_JSON)).andDo(print()).andExpect(status().isOk()) .andExpect(jsonPath(JSON_PATH_ID, equalTo(tfq.getId().intValue()))) .andExpect(jsonPath(JSON_PATH_QUERY, equalTo(filterQuery))) .andExpect(jsonPath(JSON_PATH_NAME, equalTo(filterName2))); @@ -167,7 +168,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte createSingleTargetFilterQuery(idC, testQuery); mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING)).andExpect(status().isOk()) - .andDo(MockMvcResultPrinter.print()) + .andDo(print()) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(knownTargetAmount))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(knownTargetAmount))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(knownTargetAmount))) @@ -198,7 +199,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING) .param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(limitSize))) - .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andDo(print()) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(knownTargetAmount))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(limitSize))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(limitSize))) @@ -227,7 +228,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING) .param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, String.valueOf(offsetParam)) .param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(knownTargetAmount))) - .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andDo(print()) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(knownTargetAmount))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(expectedSize))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(expectedSize))) @@ -253,7 +254,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte + tfq.getId(); // test mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId())) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(print()).andExpect(status().isOk()) .andExpect(jsonPath(JSON_PATH_NAME, equalTo(knownName))) .andExpect(jsonPath(JSON_PATH_QUERY, equalTo(knownQuery))) .andExpect(jsonPath("$._links.self.href", equalTo(hrefPrefix))) @@ -284,7 +285,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte final MvcResult mvcResult = mvc .perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING).content(notJson) .contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()).andReturn(); + .andDo(print()).andExpect(status().isBadRequest()).andReturn(); assertThat(targetFilterQueryManagement.count()).isEqualTo(0); @@ -311,7 +312,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform( post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId() + "/autoAssignDS") .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()) + .andDo(print()).andExpect(status().isForbidden()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(QuotaExceededException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); } @@ -333,7 +334,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform( post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId() + "/autoAssignDS") .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + .andDo(print()).andExpect(status().isOk()); final TargetFilterQuery updatedFilterQuery = targetFilterQueryManagement.get(filterQuery.getId()).get(); @@ -343,7 +344,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte // update the query of the filter query to trigger a quota hit mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId()) .content("{\"query\":\"controllerId==target*\"}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()) + .andDo(print()).andExpect(status().isForbidden()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(QuotaExceededException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); @@ -366,6 +367,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte verifyAutoAssignmentWithTimeForcedActionType(tfq, set); + verifyAutoAssignmentWithDownloadOnlyActionType(tfq, set); + verifyAutoAssignmentWithUnknownActionType(tfq, set); verifyAutoAssignmentWithIncompleteDs(tfq); @@ -400,7 +403,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte ? "{\"id\":" + set.getId() + ", \"type\":\"" + actionType.getName() + "\"}" : "{\"id\":" + set.getId() + "}"; mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") - .content(payload).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .content(payload).contentType(MediaType.APPLICATION_JSON)).andDo(print()) .andExpect(status().isOk()); final TargetFilterQuery updatedFilterQuery = targetFilterQueryManagement.get(tfq.getId()).get(); @@ -411,7 +414,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte .isEqualTo(MgmtRestModelMapper.convertActionType(expectedActionType)); mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId())) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(print()).andExpect(status().isOk()) .andExpect(jsonPath(JSON_PATH_NAME, equalTo(tfq.getName()))) .andExpect(jsonPath(JSON_PATH_QUERY, equalTo(tfq.getQuery()))) .andExpect(jsonPath(JSON_PATH_AUTO_ASSIGN_DS, equalTo(set.getId().intValue()))) @@ -425,7 +428,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte throws Exception { mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") .content("{\"id\":" + set.getId() + ", \"type\":\"" + MgmtActionType.TIMEFORCED.getName() + "\"}") - .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .contentType(MediaType.APPLICATION_JSON)).andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(InvalidAutoAssignActionTypeException.class.getName()))) @@ -433,12 +436,18 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte equalTo(SpServerError.SP_AUTO_ASSIGN_ACTION_TYPE_INVALID.getKey()))); } + @Step + private void verifyAutoAssignmentWithDownloadOnlyActionType(final TargetFilterQuery tfq, final DistributionSet set) + throws Exception { + verifyAutoAssignmentByActionType(tfq, set, MgmtActionType.DOWNLOAD_ONLY); + } + @Step private void verifyAutoAssignmentWithUnknownActionType(final TargetFilterQuery tfq, final DistributionSet set) throws Exception { mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") .content("{\"id\":" + set.getId() + ", \"type\":\"unknown\"}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()) + .andDo(print()).andExpect(status().isBadRequest()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(MessageNotReadableException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_REST_BODY_NOT_READABLE.getKey()))); } @@ -451,7 +460,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") .content("{\"id\":" + incompleteDistributionSet.getId() + "}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()) + .andDo(print()).andExpect(status().isBadRequest()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(InvalidAutoAssignDistributionSetException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, @@ -466,7 +475,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") .content("{\"id\":" + softDeletedDs.getId() + "}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()) + .andDo(print()).andExpect(status().isBadRequest()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(InvalidAutoAssignDistributionSetException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, 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 9e9159cbb..d8d2ce187 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 @@ -1287,6 +1287,26 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest assertThat(targetManagement.getByControllerID(target.getControllerId()).get()).isEqualTo(target); } + @Test + @Description("Verfies that a DOWNLOAD_ONLY DS to target assignment is properly handled") + public void assignDownloadOnlyDistributionSetToTarget() throws Exception { + + Target target = testdataFactory.createTarget(); + final DistributionSet set = testdataFactory.createDistributionSet("one"); + + mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + "/assignedDS") + .content("{\"id\":" + set.getId() + ",\"type\": \"downloadonly\"}") + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("assigned", equalTo(1))).andExpect(jsonPath("alreadyAssigned", equalTo(0))) + .andExpect(jsonPath("total", equalTo(1))); + + assertThat(deploymentManagement.getAssignedDistributionSet(target.getControllerId()).get()).isEqualTo(set); + Slice actions = deploymentManagement.findActionsByTarget("targetExist", PageRequest.of(0, 100)); + assertThat(actions.getSize()).isGreaterThan(0); + actions.stream().filter(a -> a.getDistributionSet().equals(set)) + .forEach(a -> ActionType.DOWNLOAD_ONLY.equals(a.getActionType())); + } + @Test @Description("Verfies that an offline DS to target assignment is reflected by the repository and that repeating " + "the assignment does not change the target.") diff --git a/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java b/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java index 7428d7b91..507f26ab3 100644 --- a/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java +++ b/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java @@ -449,12 +449,18 @@ public abstract class JsonBuilder { public static String rollout(final String name, final String description, final int groupSize, final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions) { - return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, null); + return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, null, null); } public static String rollout(final String name, final String description, final Integer groupSize, - final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions, - final List groups) { + final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions, + final List groups) { + return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, groups, null); + } + + public static String rollout(final String name, final String description, final Integer groupSize, + final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions, + final List groups, final String type) { final JSONObject json = new JSONObject(); try { json.put("name", name); @@ -463,6 +469,10 @@ public abstract class JsonBuilder { json.put("distributionSetId", distributionSetId); json.put("targetFilterQuery", targetFilterQuery); + if(type != null){ + json.put("type", type); + } + if (conditions != null) { final JSONObject successCondition = new JSONObject(); diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java index 43ee51821..2e0c0998c 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java @@ -84,6 +84,7 @@ public final class MgmtApiModelProperties { public static final String ROLLOUT_TOTAL_TARGETS = "the total targets of a rollout"; public static final String ROLLOUT_TOTAL_TARGETS_PER_STATUS = "the total targets per status"; public static final String ROLLOUT_STATUS = "the status of this rollout"; + public static final String ROLLOUT_TYPE = "the type of this rollout"; public static final String ROLLOUT_GROUP_STATUS = "the status of this rollout group"; public static final String ROLLOUT_AMOUNT_GROUPS = "the amount of groups the rollout should split targets into"; public static final String ROLLOUT_GROUPS = "the list of group definitions"; diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java index eb680bb3f..1d8f4d7ee 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java @@ -391,7 +391,7 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat requestFieldWithPath("[]maintenanceWindow.timezone") .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE).optional(), requestFieldWithPath("[]type").description(MgmtApiModelProperties.FORCETIME_TYPE) - .attributes(key("value").value("['soft', 'forced','timeforced']"))), + .attributes(key("value").value("['soft', 'forced','timeforced', 'downloadonly']"))), responseFields( fieldWithPath("assigned").description(MgmtApiModelProperties.DS_NEW_ASSIGNED_TARGETS), fieldWithPath("alreadyAssigned").type(JsonFieldType.NUMBER) diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java index 5c6923268..6e1c63526 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java @@ -130,6 +130,8 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati fieldWithPath(arrayPrefix + "distributionSetId").description(MgmtApiModelProperties.ROLLOUT_DS_ID)); allFieldDescriptor.add(fieldWithPath(arrayPrefix + "status").description(MgmtApiModelProperties.ROLLOUT_STATUS) .attributes(key("value").value("['creating','ready','paused','running','finished']"))); + allFieldDescriptor.add(fieldWithPath(arrayPrefix + "type").description(MgmtApiModelProperties.ROLLOUT_TYPE) + .attributes(key("value").value("['forced','soft','timeforced','downloadonly']"))); allFieldDescriptor.add( fieldWithPath(arrayPrefix + "totalTargets").description(MgmtApiModelProperties.ROLLOUT_TOTAL_TARGETS)); allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.self").ignored()); diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetFilterQueriesResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetFilterQueriesResourceDocumentationTest.java index 831122a97..85b846fe6 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetFilterQueriesResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetFilterQueriesResourceDocumentationTest.java @@ -81,7 +81,7 @@ public class TargetFilterQueriesResourceDocumentationTest extends AbstractApiRes fieldWithPath("content[].autoAssignActionType") .description(MgmtApiModelProperties.ACTION_FORCE_TYPE) .type(JsonFieldType.STRING.toString()) - .attributes(key("value").value("['forced', 'soft']")), + .attributes(key("value").value("['forced', 'soft', 'downloadonly']")), fieldWithPath("content[].createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), fieldWithPath("content[].createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), fieldWithPath("content[].lastModifiedAt") @@ -199,7 +199,7 @@ public class TargetFilterQueriesResourceDocumentationTest extends AbstractApiRes requestFields(requestFieldWithPath("id").description(MgmtApiModelProperties.DS_ID), optionalRequestFieldWithPath("type") .description(MgmtApiModelProperties.ACTION_FORCE_TYPE) - .attributes(key("value").value("['forced', 'soft']"))), + .attributes(key("value").value("['forced', 'soft', 'downloadonly']"))), getResponseFieldTargetFilterQuery(false))); } @@ -226,7 +226,7 @@ public class TargetFilterQueriesResourceDocumentationTest extends AbstractApiRes .type(JsonFieldType.NUMBER.toString()), fieldWithPath(arrayPrefix + "autoAssignActionType") .description(MgmtApiModelProperties.ACTION_FORCE_TYPE).type(JsonFieldType.STRING.toString()) - .attributes(key("value").value("['forced', 'soft']")), + .attributes(key("value").value("['forced', 'soft', 'downloadonly']")), fieldWithPath(arrayPrefix + "createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), fieldWithPath(arrayPrefix + "createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), fieldWithPath(arrayPrefix + "lastModifiedAt").description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT), 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 248b4093d..64236952b 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 @@ -519,7 +519,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio requestFieldWithPath("maintenanceWindow.timezone") .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE).optional(), requestFieldWithPath("type").description(MgmtApiModelProperties.FORCETIME_TYPE) - .attributes(key("value").value("['soft', 'forced','timeforced']"))), + .attributes(key("value").value("['soft', 'forced','timeforced', 'downloadonly']"))), responseFields( fieldWithPath("assigned").description(MgmtApiModelProperties.DS_NEW_ASSIGNED_TARGETS), fieldWithPath("alreadyAssigned").type(JsonFieldType.NUMBER) diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/grid/AbstractGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/grid/AbstractGrid.java index 18a1de6cc..35df61e2a 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/grid/AbstractGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/grid/AbstractGrid.java @@ -134,7 +134,7 @@ public abstract class AbstractGrid extends Grid implements Re setColumnProperties(); setColumnHeaderNames(); setColumnsHidable(); - addColumnRenderes(); + addColumnRenderers(); setColumnExpandRatio(); setHiddenColumns(); @@ -327,7 +327,7 @@ public abstract class AbstractGrid extends Grid implements Re * Template method invoked by {@link #addNewContainerDS()} for adding * special column renderers if needed. */ - protected abstract void addColumnRenderes(); + protected abstract void addColumnRenderers(); /** * Template method invoked by {@link #addNewContainerDS()} that hides 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 new file mode 100644 index 000000000..b77f739ce --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/TargetAssignmentOperations.java @@ -0,0 +1,286 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +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; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; +import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException; +import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; +import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; +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.ActionTypeOptionGroupAssignmentLayout; +import org.eclipse.hawkbit.ui.management.miscs.MaintenanceWindowLayout; +import org.eclipse.hawkbit.ui.management.state.ManagementUIState; +import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; +import org.eclipse.hawkbit.ui.utils.UINotification; +import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; +import org.slf4j.Logger; +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; +import com.vaadin.ui.Link; +import com.vaadin.ui.themes.ValoTheme; + +/** + * Helper Class for Target Assignment Operations from the Deployment View + */ +public final class TargetAssignmentOperations { + + private static final Logger LOG = LoggerFactory.getLogger(TargetAssignmentOperations.class); + + private TargetAssignmentOperations() { + } + + /** + * Save all target(s)-distributionSet assignments + * + * @param managementUIState + * the management UI state + * @param actionTypeOptionGroupLayout + * the action Type Option Group Layout + * @param maintenanceWindowLayout + * the Maintenance Window Layout + * @param deploymentManagement + * the Deployment Management + * @param notification + * the UI Notification + * @param eventBus + * the UI Event Bus + * @param i18n + * the Vaadin Message Source for multi language + * @param eventSource + * the source object for sending potential events + */ + public static void saveAllAssignments(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()); + + 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 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())); + } + } + refreshPinnedDetails(saveAssignedList, managementUIState, eventBus, eventSource); + managementUIState.getAssignedList().clear(); + notification.displaySuccess(i18n.getMessage("message.target.ds.assign.success")); + eventBus.publish(eventSource, SaveActionWindowEvent.SAVED_ASSIGNMENTS); + } + + private static void refreshPinnedDetails(final Map> saveAssignedList, + 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())) { + 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); + } + } + } + + /** + * Check wether the maintenance window is valid or not + * + * @param maintenanceWindowLayout + * the maintenance window layout + * @param notification + * the UI Notification + * @return boolean if maintenance window is valid or not + */ + public static boolean isMaintenanceWindowValid(final MaintenanceWindowLayout maintenanceWindowLayout, + final UINotification notification) { + if (maintenanceWindowLayout.isEnabled()) { + try { + MaintenanceScheduleHelper.validateMaintenanceSchedule(maintenanceWindowLayout.getMaintenanceSchedule(), + maintenanceWindowLayout.getMaintenanceDuration(), + maintenanceWindowLayout.getMaintenanceTimeZone()); + } catch (final InvalidMaintenanceScheduleException e) { + LOG.error("Maintenance window is not valid", e); + notification.displayValidationError(e.getMessage()); + return false; + } + } + return true; + } + + /** + * Create the Assignment Confirmation Tab + * + * @param actionTypeOptionGroupLayout + * the action Type Option Group Layout + * @param maintenanceWindowLayout + * the Maintenance Window Layout + * @param saveButtonToggle + * The event listener to derimne if save button should be enabled or not + * @param i18n + * the Vaadin Message Source for multi language + * @param uiProperties + * the UI Properties + * @return the Assignment Confirmation tab + */ + public static ConfirmationTab createAssignmentTab( + final ActionTypeOptionGroupAssignmentLayout actionTypeOptionGroupLayout, + final MaintenanceWindowLayout maintenanceWindowLayout, final Consumer saveButtonToggle, + final VaadinMessageSource i18n, final UiProperties uiProperties) { + + final CheckBox maintenanceWindowControl = maintenanceWindowControl(i18n, maintenanceWindowLayout, + saveButtonToggle); + final Link maintenanceWindowHelpLink = maintenanceWindowHelpLinkControl(uiProperties, i18n); + final HorizontalLayout layout = createHorizontalLayout(maintenanceWindowControl, maintenanceWindowHelpLink); + actionTypeOptionGroupLayout.selectDefaultOption(); + + initMaintenanceWindow(maintenanceWindowLayout, saveButtonToggle); + addValueChangeListener(actionTypeOptionGroupLayout, maintenanceWindowControl, maintenanceWindowHelpLink); + return createAssignmentTab(actionTypeOptionGroupLayout, layout, maintenanceWindowLayout); + } + + private static HorizontalLayout createHorizontalLayout(final CheckBox maintenanceWindowControl, + final Link maintenanceWindowHelpLink) { + final HorizontalLayout layout = new HorizontalLayout(); + layout.addComponent(maintenanceWindowControl); + layout.addComponent(maintenanceWindowHelpLink); + return layout; + } + + private static ConfirmationTab createAssignmentTab( + final ActionTypeOptionGroupAssignmentLayout actionTypeOptionGroupLayout, final HorizontalLayout layout, + final MaintenanceWindowLayout maintenanceWindowLayout) { + final ConfirmationTab assignmentTab = new ConfirmationTab(); + assignmentTab.addComponent(actionTypeOptionGroupLayout); + assignmentTab.addComponent(layout); + assignmentTab.addComponent(maintenanceWindowLayout); + return assignmentTab; + } + + private static void initMaintenanceWindow(final MaintenanceWindowLayout maintenanceWindowLayout, + final Consumer saveButtonToggle) { + maintenanceWindowLayout.setVisible(false); + maintenanceWindowLayout.setEnabled(false); + maintenanceWindowLayout.getScheduleControl().addTextChangeListener( + event -> saveButtonToggle.accept(maintenanceWindowLayout.onScheduleChange(event))); + maintenanceWindowLayout.getDurationControl().addTextChangeListener( + event -> saveButtonToggle.accept(maintenanceWindowLayout.onDurationChange(event))); + } + + private static CheckBox maintenanceWindowControl(final VaadinMessageSource i18n, + final MaintenanceWindowLayout maintenanceWindowLayout, final Consumer saveButtonToggle) { + final CheckBox enableMaintenanceWindow = new CheckBox(i18n.getMessage("caption.maintenancewindow.enabled")); + enableMaintenanceWindow.setId(UIComponentIdProvider.MAINTENANCE_WINDOW_ENABLED_ID); + enableMaintenanceWindow.addStyleName(ValoTheme.CHECKBOX_SMALL); + enableMaintenanceWindow.addStyleName("dist-window-maintenance-window-enable"); + enableMaintenanceWindow.addValueChangeListener(event -> { + final Boolean isMaintenanceWindowEnabled = enableMaintenanceWindow.getValue(); + maintenanceWindowLayout.setVisible(isMaintenanceWindowEnabled); + maintenanceWindowLayout.setEnabled(isMaintenanceWindowEnabled); + saveButtonToggle.accept(!isMaintenanceWindowEnabled); + maintenanceWindowLayout.clearAllControls(); + }); + return enableMaintenanceWindow; + } + + private static void addValueChangeListener(final ActionTypeOptionGroupAssignmentLayout actionTypeOptionGroupLayout, + final CheckBox enableMaintenanceWindowControl, final Link maintenanceWindowHelpLinkControl) { + actionTypeOptionGroupLayout.getActionTypeOptionGroup() + .addValueChangeListener(new Property.ValueChangeListener() { + private static final long serialVersionUID = 1L; + + @Override + public void valueChange(final Property.ValueChangeEvent event) { + if (event.getProperty().getValue() + .equals(AbstractActionTypeOptionGroupLayout.ActionTypeOption.DOWNLOAD_ONLY)) { + enableMaintenanceWindowControl.setValue(false); + enableMaintenanceWindowControl.setEnabled(false); + maintenanceWindowHelpLinkControl.setEnabled(false); + + } else { + enableMaintenanceWindowControl.setEnabled(true); + maintenanceWindowHelpLinkControl.setEnabled(true); + } + + } + }); + } + + private static Link maintenanceWindowHelpLinkControl(final UiProperties uiProperties, + final VaadinMessageSource i18n) { + final String maintenanceWindowHelpUrl = uiProperties.getLinks().getDocumentation().getMaintenanceWindowView(); + return SPUIComponentProvider.getHelpLink(i18n, maintenanceWindowHelpUrl); + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionHistoryGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionHistoryGrid.java index f1cdc7fa9..6ab8d39ff 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionHistoryGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionHistoryGrid.java @@ -69,8 +69,10 @@ public class ActionHistoryGrid extends AbstractGrid { private static final String STATUS_ICON_NEUTRAL = "statusIconNeutral"; private static final String STATUS_ICON_ACTIVE = "statusIconActive"; private static final String STATUS_ICON_FORCED = "statusIconForced"; + private static final String STATUS_ICON_DOWNLOAD_ONLY = "statusIconDownloadOnly"; + private static final String STATUS_ICON_SOFT = "statusIconSoft"; - private static final String VIRT_PROP_FORCED = "forced"; + private static final String VIRT_PROP_TYPE = "type"; private static final String VIRT_PROP_TIMEFORCED = "timeForced"; private static final String VIRT_PROP_ACTION_CANCEL = "cancel-action"; private static final String VIRT_PROP_ACTION_FORCE = "force-action"; @@ -79,20 +81,20 @@ public class ActionHistoryGrid extends AbstractGrid { private static final Object[] maxColumnOrder = new Object[] { ProxyAction.PXY_ACTION_IS_ACTIVE_DECO, ProxyAction.PXY_ACTION_ID, ProxyAction.PXY_ACTION_DS_NAME_VERSION, ProxyAction.PXY_ACTION_LAST_MODIFIED_AT, ProxyAction.PXY_ACTION_STATUS, ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW, - ProxyAction.PXY_ACTION_ROLLOUT_NAME, VIRT_PROP_FORCED, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, + ProxyAction.PXY_ACTION_ROLLOUT_NAME, VIRT_PROP_TYPE, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT }; private static final Object[] minColumnOrder = new Object[] { ProxyAction.PXY_ACTION_IS_ACTIVE_DECO, ProxyAction.PXY_ACTION_DS_NAME_VERSION, ProxyAction.PXY_ACTION_LAST_MODIFIED_AT, - ProxyAction.PXY_ACTION_STATUS, ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW, VIRT_PROP_FORCED, + ProxyAction.PXY_ACTION_STATUS, ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW, VIRT_PROP_TYPE, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT }; - private static final String[] leftAlignedColumns = new String[] { VIRT_PROP_TIMEFORCED }; + private static final String[] leftAlignedColumns = new String[] {}; private static final String[] centerAlignedColumns = new String[] { ProxyAction.PXY_ACTION_IS_ACTIVE_DECO, - ProxyAction.PXY_ACTION_STATUS }; + ProxyAction.PXY_ACTION_STATUS, VIRT_PROP_TYPE, ProxyAction.PXY_ACTION_ID, VIRT_PROP_TIMEFORCED }; - private static final String[] rightAlignedColumns = new String[] { VIRT_PROP_FORCED, ProxyAction.PXY_ACTION_ID }; + private static final String[] rightAlignedColumns = new String[] {}; private final transient DeploymentManagement deploymentManagement; private final UINotification notification; @@ -107,19 +109,7 @@ public class ActionHistoryGrid extends AbstractGrid { private final BeanQueryFactory targetQF = new BeanQueryFactory<>(ActionBeanQuery.class); - boolean forceClientRefreshToggle = true; - - /** - * Constructor. - * - * @param i18n - * @param deploymentManagement - * @param eventBus - * @param notification - * @param managementUIState - * @param permissionChecker - */ - protected ActionHistoryGrid(final VaadinMessageSource i18n, final DeploymentManagement deploymentManagement, + ActionHistoryGrid(final VaadinMessageSource i18n, final DeploymentManagement deploymentManagement, final UIEventBus eventBus, final UINotification notification, final ManagementUIState managementUIState, final SpPermissionChecker permissionChecker) { super(i18n, eventBus, permissionChecker); @@ -220,14 +210,14 @@ public class ActionHistoryGrid extends AbstractGrid { } @Override - protected void addColumnRenderes() { + protected void addColumnRenderers() { getColumn(ProxyAction.PXY_ACTION_LAST_MODIFIED_AT).setConverter(new LongToFormattedDateStringConverter()); getColumn(ProxyAction.PXY_ACTION_STATUS).setRenderer(new HtmlLabelRenderer(), new HtmlStatusLabelConverter(this::createStatusLabelMetadata)); getColumn(ProxyAction.PXY_ACTION_IS_ACTIVE_DECO).setRenderer(new HtmlLabelRenderer(), new HtmlIsActiveLabelConverter(this::createIsActiveLabelMetadata)); - getColumn(VIRT_PROP_FORCED).setRenderer(new HtmlLabelRenderer(), - new HtmlVirtPropLabelConverter(this::createForcedLabelMetadata)); + getColumn(VIRT_PROP_TYPE).setRenderer(new HtmlLabelRenderer(), + new HtmlVirtPropLabelConverter(this::createTypeLabelMetadata)); getColumn(VIRT_PROP_TIMEFORCED).setRenderer(new HtmlLabelRenderer(), new HtmlVirtPropLabelConverter(this::createTimeForcedLabelMetadata)); getColumn(VIRT_PROP_ACTION_CANCEL).setRenderer( @@ -270,14 +260,23 @@ public class ActionHistoryGrid extends AbstractGrid { return activeStates.get(isActiveDeco); } - private StatusFontIcon createForcedLabelMetadata(final Action action) { - StatusFontIcon result = null; + private StatusFontIcon createTypeLabelMetadata(final Action action) { if (ActionType.FORCED.equals(action.getActionType()) || ActionType.TIMEFORCED.equals(action.getActionType())) { - result = new StatusFontIcon(FontAwesome.BOLT, STATUS_ICON_FORCED, + return new StatusFontIcon(FontAwesome.BOLT, STATUS_ICON_FORCED, i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_FORCED), - UIComponentIdProvider.ACTION_HISTORY_TABLE_FORCED_LABEL_ID); + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); } - return result; + if (ActionType.SOFT.equals(action.getActionType())) { + return new StatusFontIcon(FontAwesome.STEP_FORWARD, STATUS_ICON_SOFT, + i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_SOFT), + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); + } + if (ActionType.DOWNLOAD_ONLY.equals(action.getActionType())) { + return new StatusFontIcon(FontAwesome.DOWNLOAD, STATUS_ICON_DOWNLOAD_ONLY, + i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_DOWNLOAD_ONLY), + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); + } + return null; } private StatusFontIcon createTimeForcedLabelMetadata(final Action action) { @@ -408,9 +407,6 @@ public class ActionHistoryGrid extends AbstractGrid { eventBus.publish(this, PinUnpinEvent.PIN_TARGET); } }); - if (!managementUIState.getDistributionTableFilters().getPinnedTarget().isPresent()) { - return; - } } // service call to cancel the active action @@ -443,7 +439,7 @@ public class ActionHistoryGrid extends AbstractGrid { @Override protected void setHiddenColumns() { - getColumn(VIRT_PROP_FORCED).setHidable(false); + getColumn(VIRT_PROP_TYPE).setHidable(false); getColumn(VIRT_PROP_TIMEFORCED).setHidable(false); getColumn(VIRT_PROP_ACTION_CANCEL).setHidable(false); getColumn(VIRT_PROP_ACTION_FORCE).setHidable(false); @@ -470,10 +466,8 @@ public class ActionHistoryGrid extends AbstractGrid { getColumn(ProxyAction.PXY_ACTION_STATUS).setHeaderCaption(i18n.getMessage("header.status")); getColumn(ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW) .setHeaderCaption(i18n.getMessage("header.maintenancewindow")); - getColumn(VIRT_PROP_FORCED).setHeaderCaption(String.valueOf(forceClientRefreshToggle)); - forceClientRefreshToggle = !forceClientRefreshToggle; - newHeaderRow.join(VIRT_PROP_FORCED, VIRT_PROP_TIMEFORCED).setText(i18n.getMessage("label.action.forced")); + newHeaderRow.join(VIRT_PROP_TYPE, VIRT_PROP_TIMEFORCED).setText(i18n.getMessage("label.action.type")); newHeaderRow.join(VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT) .setText(i18n.getMessage("header.action")); } @@ -485,7 +479,7 @@ public class ActionHistoryGrid extends AbstractGrid { setColumnsSize(100.0, 130.0, ProxyAction.PXY_ACTION_LAST_MODIFIED_AT); setColumnsSize(53.0, 55.0, ProxyAction.PXY_ACTION_STATUS); setColumnsSize(150.0, 200.0, ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW); - setColumnsSize(FIXED_PIX_MIN, FIXED_PIX_MIN, VIRT_PROP_FORCED, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, + setColumnsSize(FIXED_PIX_MIN, FIXED_PIX_MIN, VIRT_PROP_TYPE, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT); } @@ -569,7 +563,7 @@ public class ActionHistoryGrid extends AbstractGrid { protected GeneratedPropertyContainer addGeneratedContainerProperties() { final GeneratedPropertyContainer decoratedContainer = getDecoratedContainer(); - decoratedContainer.addGeneratedProperty(VIRT_PROP_FORCED, new GenericPropertyValueGenerator()); + decoratedContainer.addGeneratedProperty(VIRT_PROP_TYPE, new GenericPropertyValueGenerator()); decoratedContainer.addGeneratedProperty(VIRT_PROP_TIMEFORCED, new GenericPropertyValueGenerator()); decoratedContainer.addGeneratedProperty(VIRT_PROP_ACTION_CANCEL, new GenericPropertyValueGenerator()); decoratedContainer.addGeneratedProperty(VIRT_PROP_ACTION_FORCE, new GenericPropertyValueGenerator()); @@ -622,7 +616,7 @@ public class ActionHistoryGrid extends AbstractGrid { setColumnsSize(107.0, 500.0, ProxyAction.PXY_ACTION_DS_NAME_VERSION); setColumnsSize(100.0, 150.0, ProxyAction.PXY_ACTION_LAST_MODIFIED_AT); setColumnsSize(53.0, 55.0, ProxyAction.PXY_ACTION_STATUS); - setColumnsSize(FIXED_PIX_MIN, FIXED_PIX_MAX, VIRT_PROP_FORCED, VIRT_PROP_TIMEFORCED, + setColumnsSize(FIXED_PIX_MIN, FIXED_PIX_MAX, VIRT_PROP_TYPE, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT); setColumnsSize(FIXED_PIX_MIN, 500.0, ProxyAction.PXY_ACTION_ROLLOUT_NAME); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusGrid.java index 2b774659f..385066285 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusGrid.java @@ -133,7 +133,7 @@ public class ActionStatusGrid extends AbstractGrid { } @Override - protected void addColumnRenderes() { + protected void addColumnRenderers() { getColumn(ProxyActionStatus.PXY_AS_STATUS).setRenderer(new HtmlLabelRenderer(), new HtmlStatusLabelConverter(this::createStatusLabelMetadata)); getColumn(ProxyActionStatus.PXY_AS_CREATED_AT).setConverter(new LongToFormattedDateStringConverter()); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusMsgGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusMsgGrid.java index 47d4bcbfb..43afb4c7e 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusMsgGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusMsgGrid.java @@ -146,7 +146,7 @@ public class ActionStatusMsgGrid extends AbstractGrid { } @Override - protected void addColumnRenderes() { + protected void addColumnRenderers() { // no specific column renderers } 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 095af4519..aa13acd43 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 @@ -8,6 +8,10 @@ */ package org.eclipse.hawkbit.ui.management.dstable; +import static org.eclipse.hawkbit.ui.management.TargetAssignmentOperations.createAssignmentTab; +import static org.eclipse.hawkbit.ui.management.TargetAssignmentOperations.isMaintenanceWindowValid; +import static org.eclipse.hawkbit.ui.management.TargetAssignmentOperations.saveAllAssignments; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -18,27 +22,21 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; -import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetTagManagement; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent; -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.DistributionSetTagAssignmentResult; -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.SpPermissionChecker; import org.eclipse.hawkbit.ui.UiProperties; import org.eclipse.hawkbit.ui.common.ConfirmationDialog; -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.common.table.AbstractNamedVersionTable; @@ -49,8 +47,6 @@ import org.eclipse.hawkbit.ui.management.event.DistributionTableEvent; import org.eclipse.hawkbit.ui.management.event.ManagementUIEvent; import org.eclipse.hawkbit.ui.management.event.PinUnpinEvent; import org.eclipse.hawkbit.ui.management.event.RefreshDistributionTableByFilterEvent; -import org.eclipse.hawkbit.ui.management.event.SaveActionWindowEvent; -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; @@ -66,8 +62,6 @@ import org.eclipse.hawkbit.ui.utils.UIMessageIdProvider; import org.eclipse.hawkbit.ui.utils.UINotification; import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; import org.eclipse.hawkbit.ui.view.filter.OnlyEventsFromDeploymentViewFilter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -86,13 +80,9 @@ import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; import com.vaadin.server.FontAwesome; import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickEvent; -import com.vaadin.ui.CheckBox; import com.vaadin.ui.DragAndDropWrapper; -import com.vaadin.ui.HorizontalLayout; -import com.vaadin.ui.Link; import com.vaadin.ui.Table; import com.vaadin.ui.UI; -import com.vaadin.ui.themes.ValoTheme; /** * Distribution set table which is shown on the Deployment View. @@ -101,8 +91,6 @@ public class DistributionTable extends AbstractNamedVersionTable { - if (ok && isMaintenanceWindowValid()) { - saveAllAssignments(); + if (ok && isMaintenanceWindowValid(maintenanceWindowLayout, getNotification())) { + saveAllAssignments(managementUIState, actionTypeOptionGroupLayout, maintenanceWindowLayout, + deploymentManagement, getNotification(), getEventBus(), getI18n(), this); } else { managementUIState.getAssignedList().clear(); } - }, createAssignmentTab(), UIComponentIdProvider.DIST_SET_TO_TARGET_ASSIGNMENT_CONFIRM_ID); + }, 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, @@ -489,145 +484,6 @@ public class DistributionTable extends AbstractNamedVersionTable itemIds = managementUIState.getAssignedList().keySet(); - Long distId; - List targetIdSetList; - List tempIdList; - final ActionType actionType = ((ActionTypeOption) actionTypeOptionGroupLayout.getActionTypeOptionGroup() - .getValue()).getActionType(); - final long forcedTimeStamp = (((ActionTypeOption) actionTypeOptionGroupLayout.getActionTypeOptionGroup() - .getValue()) == ActionTypeOption.AUTO_FORCED) - ? actionTypeOptionGroupLayout.getForcedTimeDateField().getValue().getTime() - : RepositoryModelConstants.NO_FORCE_TIME; - - final Map> saveAssignedList = Maps.newHashMapWithExpectedSize(itemIds.size()); - - 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 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) { - getNotification().displaySuccess(getI18n().getMessage("message.target.assignment", - distributionSetAssignmentResult.getAssigned())); - } - if (distributionSetAssignmentResult.getAlreadyAssigned() > 0) { - getNotification().displaySuccess(getI18n().getMessage("message.target.alreadyAssigned", - distributionSetAssignmentResult.getAlreadyAssigned())); - } - } - resfreshPinnedDetails(saveAssignedList); - - managementUIState.getAssignedList().clear(); - getNotification().displaySuccess(getI18n().getMessage("message.target.ds.assign.success")); - getEventBus().publish(this, SaveActionWindowEvent.SAVED_ASSIGNMENTS); - } - - private void resfreshPinnedDetails(final Map> saveAssignedList) { - final Optional pinnedDist = managementUIState.getTargetTableFilters().getPinnedDistId(); - final Optional pinnedTarget = managementUIState.getDistributionTableFilters().getPinnedTarget(); - - if (pinnedDist.isPresent()) { - if (saveAssignedList.keySet().contains(pinnedDist.get())) { - getEventBus().publish(this, PinUnpinEvent.PIN_DISTRIBUTION); - } - } else if (pinnedTarget.isPresent()) { - final Set assignedTargetIds = managementUIState.getAssignedList().keySet(); - if (assignedTargetIds.contains(pinnedTarget.get())) { - getEventBus().publish(this, PinUnpinEvent.PIN_TARGET); - } - } - } - - private ConfirmationTab createAssignmentTab() { - final ConfirmationTab assignmentTab = new ConfirmationTab(); - actionTypeOptionGroupLayout.selectDefaultOption(); - assignmentTab.addComponent(actionTypeOptionGroupLayout); - assignmentTab.addComponent(enableMaintenanceWindowLayout()); - initMaintenanceWindow(); - assignmentTab.addComponent(maintenanceWindowLayout); - return assignmentTab; - } - - private HorizontalLayout enableMaintenanceWindowLayout() { - final HorizontalLayout layout = new HorizontalLayout(); - layout.addComponent(enableMaintenanceWindowControl()); - layout.addComponent(maintenanceWindowHelpLinkControl()); - return layout; - } - - private CheckBox enableMaintenanceWindowControl() { - final CheckBox enableMaintenanceWindow = new CheckBox( - getI18n().getMessage("caption.maintenancewindow.enabled")); - enableMaintenanceWindow.setId(UIComponentIdProvider.MAINTENANCE_WINDOW_ENABLED_ID); - enableMaintenanceWindow.addStyleName(ValoTheme.CHECKBOX_SMALL); - enableMaintenanceWindow.addStyleName("dist-window-maintenance-window-enable"); - enableMaintenanceWindow.addValueChangeListener(event -> { - final Boolean isMaintenanceWindowEnabled = enableMaintenanceWindow.getValue(); - maintenanceWindowLayout.setVisible(isMaintenanceWindowEnabled); - maintenanceWindowLayout.setEnabled(isMaintenanceWindowEnabled); - enableSaveButton(!isMaintenanceWindowEnabled); - maintenanceWindowLayout.clearAllControls(); - }); - return enableMaintenanceWindow; - } - - private Link maintenanceWindowHelpLinkControl() { - final String maintenanceWindowHelpUrl = uiProperties.getLinks().getDocumentation().getMaintenanceWindowView(); - return SPUIComponentProvider.getHelpLink(getI18n(), maintenanceWindowHelpUrl); - } - - private void initMaintenanceWindow() { - maintenanceWindowLayout.setVisible(false); - maintenanceWindowLayout.setEnabled(false); - maintenanceWindowLayout.getScheduleControl() - .addTextChangeListener(event -> enableSaveButton(maintenanceWindowLayout.onScheduleChange(event))); - maintenanceWindowLayout.getDurationControl() - .addTextChangeListener(event -> enableSaveButton(maintenanceWindowLayout.onDurationChange(event))); - } - - private void enableSaveButton(final boolean enabled) { - confirmDialog.getOkButton().setEnabled(enabled); - } - @Override protected List hasMissingPermissionsForDrop() { return permissionChecker.hasUpdateTargetPermission() ? Collections.emptyList() diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/AbstractActionTypeOptionGroupLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/AbstractActionTypeOptionGroupLayout.java index ddbc4ca7d..53efaa1ab 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/AbstractActionTypeOptionGroupLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/AbstractActionTypeOptionGroupLayout.java @@ -78,9 +78,25 @@ public abstract class AbstractActionTypeOptionGroupLayout extends HorizontalLayo softLabel.setCaption(i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_SOFT)); softLabel.setDescription(i18n.getMessage(UIMessageIdProvider.TOOLTIP_SOFT_ITEM)); softLabel.setStyleName("padding-right-style"); + softLabel.setIcon(FontAwesome.STEP_FORWARD); addComponent(softLabel); } + protected void addDownloadOnlyItemWithLabel() { + final FlexibleOptionGroupItemComponent downloadOnlyItem = actionTypeOptionGroup + .getItemComponent(ActionTypeOption.DOWNLOAD_ONLY); + downloadOnlyItem.setId(UIComponentIdProvider.ACTION_DETAILS_DOWNLOAD_ONLY_ID); + downloadOnlyItem.setStyleName(STYLE_DIST_WINDOW_ACTIONTYPE); + addComponent(downloadOnlyItem); + final Label downloadOnlyLabel = new Label(); + downloadOnlyLabel.setSizeFull(); + downloadOnlyLabel.setCaption(i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_DOWNLOAD_ONLY)); + downloadOnlyLabel.setDescription(i18n.getMessage(UIMessageIdProvider.TOOLTIP_DOWNLOAD_ONLY_ITEM)); + downloadOnlyLabel.setStyleName("padding-right-style"); + downloadOnlyLabel.setIcon(FontAwesome.DOWNLOAD); + addComponent(downloadOnlyLabel); + } + /** * To Set Default option for save. */ @@ -93,7 +109,8 @@ public abstract class AbstractActionTypeOptionGroupLayout extends HorizontalLayo * */ public enum ActionTypeOption { - FORCED(ActionType.FORCED), SOFT(ActionType.SOFT), AUTO_FORCED(ActionType.TIMEFORCED); + FORCED(ActionType.FORCED), SOFT(ActionType.SOFT), AUTO_FORCED(ActionType.TIMEFORCED), + DOWNLOAD_ONLY(ActionType.DOWNLOAD_ONLY); private final ActionType actionType; diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAssignmentLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAssignmentLayout.java index 6773ef522..36837db56 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAssignmentLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAssignmentLayout.java @@ -70,13 +70,17 @@ public class ActionTypeOptionGroupAssignmentLayout extends AbstractActionTypeOpt actionTypeOptionGroup.addItem(ActionTypeOption.SOFT); actionTypeOptionGroup.addItem(ActionTypeOption.FORCED); actionTypeOptionGroup.addItem(ActionTypeOption.AUTO_FORCED); + actionTypeOptionGroup.addItem(ActionTypeOption.DOWNLOAD_ONLY); selectDefaultOption(); addForcedItemWithLabel(); addSoftItemWithLabel(); addAutoForceItemWithLabelAndDateField(); + addDownloadOnlyItemWithLabel(); } + + private void addAutoForceItemWithLabelAndDateField() { final FlexibleOptionGroupItemComponent autoForceItem = actionTypeOptionGroup .getItemComponent(ActionTypeOption.AUTO_FORCED); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAutoAssignmentLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAutoAssignmentLayout.java index d7b518524..c12da1282 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAutoAssignmentLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAutoAssignmentLayout.java @@ -32,9 +32,11 @@ public class ActionTypeOptionGroupAutoAssignmentLayout extends AbstractActionTyp actionTypeOptionGroup = new FlexibleOptionGroup(); actionTypeOptionGroup.addItem(ActionTypeOption.SOFT); actionTypeOptionGroup.addItem(ActionTypeOption.FORCED); + actionTypeOptionGroup.addItem(ActionTypeOption.DOWNLOAD_ONLY); selectDefaultOption(); addForcedItemWithLabel(); addSoftItemWithLabel(); + addDownloadOnlyItemWithLabel(); } } 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 cc48ea94f..a424b595b 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 @@ -8,6 +8,10 @@ */ package org.eclipse.hawkbit.ui.management.targettable; +import static org.eclipse.hawkbit.ui.management.TargetAssignmentOperations.createAssignmentTab; +import static org.eclipse.hawkbit.ui.management.TargetAssignmentOperations.isMaintenanceWindowValid; +import static org.eclipse.hawkbit.ui.management.TargetAssignmentOperations.saveAllAssignments; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -19,6 +23,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -26,32 +31,24 @@ import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.FilterParams; -import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetTagManagement; import org.eclipse.hawkbit.repository.event.remote.entity.RemoteEntityEvent; -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.Tag; import org.eclipse.hawkbit.repository.model.Target; 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.repository.model.TargetWithActionType; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.UiProperties; import org.eclipse.hawkbit.ui.common.ConfirmationDialog; import org.eclipse.hawkbit.ui.common.ManagementEntityState; import org.eclipse.hawkbit.ui.common.UserDetailsFormatter; -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.common.table.AbstractTable; import org.eclipse.hawkbit.ui.common.table.BaseEntityEventType; -import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; import org.eclipse.hawkbit.ui.dd.criteria.ManagementViewClientCriterion; import org.eclipse.hawkbit.ui.management.event.ManagementUIEvent; import org.eclipse.hawkbit.ui.management.event.PinUnpinEvent; @@ -60,7 +57,6 @@ import org.eclipse.hawkbit.ui.management.event.TargetAddUpdateWindowEvent; import org.eclipse.hawkbit.ui.management.event.TargetFilterEvent; import org.eclipse.hawkbit.ui.management.event.TargetTableEvent; import org.eclipse.hawkbit.ui.management.event.TargetTableEvent.TargetComponentEvent; -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; @@ -99,11 +95,8 @@ import com.vaadin.server.FontAwesome; import com.vaadin.shared.ui.label.ContentMode; import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickEvent; -import com.vaadin.ui.CheckBox; import com.vaadin.ui.DragAndDropWrapper; -import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Label; -import com.vaadin.ui.Link; import com.vaadin.ui.Table; import com.vaadin.ui.UI; import com.vaadin.ui.themes.ValoTheme; @@ -379,8 +372,7 @@ public class TargetTable extends AbstractTable { if ( isFilteredByTags()) { - final List list = new ArrayList<>(); - list.addAll(managementUIState.getTargetTableFilters().getClickedTargetTags()); + final List list = new ArrayList<>(managementUIState.getTargetTableFilters().getClickedTargetTags()); queryConfig.put(SPUIDefinitions.FILTER_BY_TAG, list.toArray(new String[list.size()])); } if (isFilteredByStatus()) { @@ -524,7 +516,7 @@ public class TargetTable extends AbstractTable { } private void tagAssignment(final DragAndDropEvent event) { - final List targetList = getDraggedTargetList(event).stream().collect(Collectors.toList()); + final List targetList = new ArrayList<>(getDraggedTargetList(event)); final String targTagName = HawkbitCommonUtil.removePrefix(event.getTransferable().getSourceComponent().getId(), SPUIDefinitions.TARGET_TAG_ID_PREFIXS); @@ -858,95 +850,6 @@ public class TargetTable extends AbstractTable { return !managementUIState.getTargetTableFilters().getClickedTargetTags().isEmpty(); } - // Code for assignment start - private void saveAllAssignments() { - final Set itemIds = managementUIState.getAssignedList().keySet(); - Long distId; - List targetIdSetList; - List tempIdList; - final ActionType actionType = ((ActionTypeOption) actionTypeOptionGroupLayout.getActionTypeOptionGroup() - .getValue()).getActionType(); - final long forcedTimeStamp = (((ActionTypeOption) actionTypeOptionGroupLayout.getActionTypeOptionGroup() - .getValue()) == ActionTypeOption.AUTO_FORCED) - ? actionTypeOptionGroupLayout.getForcedTimeDateField().getValue().getTime() - : RepositoryModelConstants.NO_FORCE_TIME; - - final Map> saveAssignedList = Maps.newHashMapWithExpectedSize(itemIds.size()); - - 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 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) { - getNotification().displaySuccess(getI18n().getMessage("message.target.assignment", - distributionSetAssignmentResult.getAssigned())); - } - if (distributionSetAssignmentResult.getAlreadyAssigned() > 0) { - getNotification().displaySuccess(getI18n().getMessage("message.target.alreadyAssigned", - distributionSetAssignmentResult.getAlreadyAssigned())); - } - } - resfreshPinnedDetails(saveAssignedList); - - managementUIState.getAssignedList().clear(); - getNotification().displaySuccess(getI18n().getMessage("message.target.ds.assign.success")); - getEventBus().publish(this, SaveActionWindowEvent.SAVED_ASSIGNMENTS); - } - - private void resfreshPinnedDetails(final Map> saveAssignedList) { - final Optional pinnedDist = managementUIState.getTargetTableFilters().getPinnedDistId(); - final Optional pinnedTarget = managementUIState.getDistributionTableFilters().getPinnedTarget(); - - if (pinnedDist.isPresent()) { - if (saveAssignedList.keySet().contains(pinnedDist.get())) { - getEventBus().publish(this, PinUnpinEvent.PIN_DISTRIBUTION); - } - } else if (pinnedTarget.isPresent()) { - final Set assignedTargetIds = managementUIState.getAssignedList().keySet(); - if (assignedTargetIds.contains(pinnedTarget.get())) { - getEventBus().publish(this, PinUnpinEvent.PIN_TARGET); - } - } - } - - private boolean isMaintenanceWindowValid() { - if (maintenanceWindowLayout.isEnabled()) { - try { - MaintenanceScheduleHelper.validateMaintenanceSchedule(maintenanceWindowLayout.getMaintenanceSchedule(), - maintenanceWindowLayout.getMaintenanceDuration(), - maintenanceWindowLayout.getMaintenanceTimeZone()); - } catch (final InvalidMaintenanceScheduleException e) { - LOG.error("Maintenance window is not valid", e); - getNotification().displayValidationError(e.getMessage()); - return false; - } - } - return true; - } - private void assignDsToTarget(final DragAndDropEvent event) { final TableTransferable transferable = (TableTransferable) event.getTransferable(); final AbstractTable source = (AbstractTable) transferable.getSourceComponent(); @@ -988,65 +891,21 @@ public class TargetTable extends AbstractTable { getI18n().getMessage(MESSAGE_CONFIRM_ASSIGN_ENTITY, distributionNameToAssign, "target", targetName), getI18n().getMessage(UIMessageIdProvider.BUTTON_OK), getI18n().getMessage(UIMessageIdProvider.BUTTON_CANCEL), ok -> { - if (ok && isMaintenanceWindowValid()) { - saveAllAssignments(); + if (ok && isMaintenanceWindowValid(maintenanceWindowLayout, getNotification())) { + saveAllAssignments(managementUIState, actionTypeOptionGroupLayout, maintenanceWindowLayout, + deploymentManagement, getNotification(), getEventBus(), getI18n(), this); } else { managementUIState.getAssignedList().clear(); } - }, createAssignmentTab(), UIComponentIdProvider.DIST_SET_TO_TARGET_ASSIGNMENT_CONFIRM_ID); + }, createAssignmentTab(actionTypeOptionGroupLayout, maintenanceWindowLayout, saveButtonToggle(), + getI18n(), uiProperties), + UIComponentIdProvider.DIST_SET_TO_TARGET_ASSIGNMENT_CONFIRM_ID); UI.getCurrent().addWindow(confirmDialog.getWindow()); confirmDialog.getWindow().bringToFront(); } - private ConfirmationTab createAssignmentTab() { - final ConfirmationTab assignmentTab = new ConfirmationTab(); - actionTypeOptionGroupLayout.selectDefaultOption(); - assignmentTab.addComponent(actionTypeOptionGroupLayout); - assignmentTab.addComponent(enableMaintenanceWindowLayout()); - initMaintenanceWindow(); - assignmentTab.addComponent(maintenanceWindowLayout); - return assignmentTab; - } - - private HorizontalLayout enableMaintenanceWindowLayout() { - final HorizontalLayout layout = new HorizontalLayout(); - layout.addComponent(enableMaintenanceWindowControl()); - layout.addComponent(maintenanceWindowHelpLinkControl()); - return layout; - } - - private CheckBox enableMaintenanceWindowControl() { - final CheckBox enableMaintenanceWindow = new CheckBox( - getI18n().getMessage("caption.maintenancewindow.enabled")); - enableMaintenanceWindow.setId(UIComponentIdProvider.MAINTENANCE_WINDOW_ENABLED_ID); - enableMaintenanceWindow.addStyleName(ValoTheme.CHECKBOX_SMALL); - enableMaintenanceWindow.addStyleName("dist-window-maintenance-window-enable"); - enableMaintenanceWindow.addValueChangeListener(event -> { - final Boolean isMaintenanceWindowEnabled = enableMaintenanceWindow.getValue(); - maintenanceWindowLayout.setVisible(isMaintenanceWindowEnabled); - maintenanceWindowLayout.setEnabled(isMaintenanceWindowEnabled); - enableSaveButton(!isMaintenanceWindowEnabled); - maintenanceWindowLayout.clearAllControls(); - }); - return enableMaintenanceWindow; - } - - private Link maintenanceWindowHelpLinkControl() { - final String maintenanceWindowHelpUrl = uiProperties.getLinks().getDocumentation().getMaintenanceWindowView(); - return SPUIComponentProvider.getHelpLink(getI18n(), maintenanceWindowHelpUrl); - } - - private void initMaintenanceWindow() { - maintenanceWindowLayout.setVisible(false); - maintenanceWindowLayout.setEnabled(false); - maintenanceWindowLayout.getScheduleControl() - .addTextChangeListener(event -> enableSaveButton(maintenanceWindowLayout.onScheduleChange(event))); - maintenanceWindowLayout.getDurationControl() - .addTextChangeListener(event -> enableSaveButton(maintenanceWindowLayout.onDurationChange(event))); - } - - private void enableSaveButton(final boolean enabled) { - confirmDialog.getOkButton().setEnabled(enabled); + private Consumer saveButtonToggle() { + return isEnabled -> confirmDialog.getOkButton().setEnabled(isEnabled); } private void addNewTargetToAssignmentList(final TargetIdName createTargetIdName, diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableLayout.java index b5fd2c5b9..754506494 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableLayout.java @@ -36,10 +36,6 @@ public class TargetTableLayout extends AbstractTableLayout { private final transient EventBus.UIEventBus eventBus; - private final TargetDetails targetDetails; - - private final TargetTableHeader targetTableHeader; - public TargetTableLayout(final UIEventBus eventBus, final TargetTable targetTable, final TargetManagement targetManagement, final EntityFactory entityFactory, final VaadinMessageSource i18n, final UINotification uiNotification, final ManagementUIState managementUIState, @@ -50,10 +46,10 @@ public class TargetTableLayout extends AbstractTableLayout { final TargetMetadataPopupLayout targetMetadataPopupLayout = new TargetMetadataPopupLayout(i18n, uiNotification, eventBus, targetManagement, entityFactory, permissionChecker); this.eventBus = eventBus; - this.targetDetails = new TargetDetails(i18n, eventBus, permissionChecker, managementUIState, uiNotification, + TargetDetails targetDetails = new TargetDetails(i18n, eventBus, permissionChecker, managementUIState, uiNotification, tagManagement, targetManagement, targetMetadataPopupLayout, deploymentManagement, entityFactory, targetTable); - this.targetTableHeader = new TargetTableHeader(i18n, permissionChecker, eventBus, uiNotification, + TargetTableHeader targetTableHeader = new TargetTableHeader(i18n, permissionChecker, eventBus, uiNotification, managementUIState, managementViewClientCriterion, targetManagement, deploymentManagement, uiProperties, entityFactory, uiNotification, tagManagement, distributionSetManagement, uiExecutor, targetTable); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java index e44ffc9d9..e92e1c32d 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java @@ -392,6 +392,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { private CommonDialogWindow createWindow() { return new WindowBuilder(SPUIDefinitions.CREATE_UPDATE_WINDOW) .caption(i18n.getMessage("caption.create.new", i18n.getMessage("caption.rollout"))).content(this) + .id(UIComponentIdProvider.ROLLOUT_POPUP_ID) .layout(this).i18n(i18n).helpLink(uiProperties.getLinks().getDocumentation().getRolloutView()) .saveDialogCloseListener(new SaveOnDialogCloseListener()).buildCommonDialogWindow(); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/ProxyRollout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/ProxyRollout.java index e1a6a3b01..0409c3a5d 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/ProxyRollout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/ProxyRollout.java @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.ui.rollout.rollout; +import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus; import org.eclipse.hawkbit.ui.customrenderers.client.renderers.RolloutRendererData; @@ -45,6 +46,15 @@ public class ProxyRollout { private TotalTargetCountStatus totalTargetCountStatus; private String approvalDecidedBy; private String approvalRemark; + private ActionType actionType; + + public ActionType getActionType() { + return actionType; + } + + public void setActionType(final ActionType actionType) { + this.actionType = actionType; + } public RolloutRendererData getRolloutRendererData() { return rolloutRendererData; diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutBeanQuery.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutBeanQuery.java index fa0fa04e0..5d0ad67a9 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutBeanQuery.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutBeanQuery.java @@ -118,6 +118,7 @@ public class RolloutBeanQuery extends AbstractBeanQuery { proxyRollout.setForcedTime(rollout.getForcedTime()); proxyRollout.setId(rollout.getId()); proxyRollout.setStatus(rollout.getStatus()); + proxyRollout.setActionType(rollout.getActionType()); proxyRollout.setRolloutRendererData(new RolloutRendererData(rollout.getName(), rollout.getStatus().toString())); final TotalTargetCountStatus totalTargetCountActionStatus = rollout.getTotalTargetCountStatus(); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java index 841f21c8a..d3df87de5 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java @@ -26,6 +26,7 @@ import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus; @@ -38,6 +39,7 @@ import org.eclipse.hawkbit.ui.common.ConfirmationDialog; import org.eclipse.hawkbit.ui.common.grid.AbstractGrid; import org.eclipse.hawkbit.ui.customrenderers.client.renderers.RolloutRendererData; import org.eclipse.hawkbit.ui.customrenderers.renderers.AbstractGridButtonConverter; +import org.eclipse.hawkbit.ui.customrenderers.renderers.AbstractHtmlLabelConverter; import org.eclipse.hawkbit.ui.customrenderers.renderers.GridButtonRenderer; import org.eclipse.hawkbit.ui.customrenderers.renderers.HtmlLabelRenderer; import org.eclipse.hawkbit.ui.customrenderers.renderers.RolloutRenderer; @@ -87,6 +89,10 @@ public class RolloutListGrid extends AbstractGrid { private static final String VIRT_PROP_UPDATE = "update"; private static final String VIRT_PROP_COPY = "copy"; private static final String VIRT_PROP_DELETE = "delete"; + private static final String STATUS_ICON_DOWNLOAD_ONLY = "statusIconDownloadOnly"; + private static final String STATUS_ICON_SOFT = "statusIconSoft"; + private static final String STATUS_ICON_FORCED = "statusIconForced"; + private static final String PROP_TYPE = "actionType"; private final transient RolloutManagement rolloutManagement; @@ -100,6 +106,8 @@ public class RolloutListGrid extends AbstractGrid { private final RolloutUIState rolloutUIState; + private final AlignCellStyleGenerator alignGenerator; + private static final List DELETE_COPY_BUTTON_ENABLED = Arrays.asList(RolloutStatus.CREATING, RolloutStatus.ERROR_CREATING, RolloutStatus.ERROR_STARTING, RolloutStatus.PAUSED, RolloutStatus.READY, RolloutStatus.RUNNING, RolloutStatus.STARTING, RolloutStatus.STOPPED, RolloutStatus.FINISHED, @@ -109,7 +117,7 @@ public class RolloutListGrid extends AbstractGrid { RolloutStatus.ERROR_CREATING, RolloutStatus.ERROR_STARTING, RolloutStatus.PAUSED, RolloutStatus.READY, RolloutStatus.RUNNING, RolloutStatus.STARTING, RolloutStatus.STOPPED); - private static final List PAUSE_BUTTON_ENABLED = Arrays.asList(RolloutStatus.RUNNING); + private static final List PAUSE_BUTTON_ENABLED = Collections.singletonList(RolloutStatus.RUNNING); private static final List RUN_BUTTON_ENABLED = Arrays.asList(RolloutStatus.READY, RolloutStatus.PAUSED); @@ -119,11 +127,13 @@ public class RolloutListGrid extends AbstractGrid { private static final Map statusIconMap = new EnumMap<>(RolloutStatus.class); - private static final List HIDDEN_COLUMNS = Arrays.asList(SPUILabelDefinitions.VAR_CREATED_DATE, + private static final List HIDDEN_COLUMNS = Arrays.asList(PROP_TYPE, SPUILabelDefinitions.VAR_CREATED_DATE, SPUILabelDefinitions.VAR_CREATED_USER, SPUILabelDefinitions.VAR_MODIFIED_DATE, SPUILabelDefinitions.VAR_MODIFIED_BY, SPUILabelDefinitions.VAR_APPROVAL_DECIDED_BY, SPUILabelDefinitions.VAR_APPROVAL_REMARK, SPUILabelDefinitions.VAR_DESC); + private static final String[] centerAlignedColumns = new String[] { PROP_TYPE }; + static { statusIconMap.put(RolloutStatus.FINISHED, new StatusFontIcon(FontAwesome.CHECK_CIRCLE, SPUIStyleDefinitions.STATUS_ICON_GREEN)); @@ -163,6 +173,7 @@ public class RolloutListGrid extends AbstractGrid { rolloutGroupManagement, quotaManagement); this.uiNotification = uiNotification; this.rolloutUIState = rolloutUIState; + alignGenerator = new AlignCellStyleGenerator(null, centerAlignedColumns, null); setGeneratedPropertySupport(new RolloutGeneratedPropertySupport()); init(); @@ -183,7 +194,7 @@ public class RolloutListGrid extends AbstractGrid { refreshContainer(); break; default: - return; + break; } } @@ -231,6 +242,7 @@ public class RolloutListGrid extends AbstractGrid { private void updateItem(final Rollout rollout, final Item item) { final TotalTargetCountStatus totalTargetCountStatus = rollout.getTotalTargetCountStatus(); item.getItemProperty(SPUILabelDefinitions.VAR_STATUS).setValue(rollout.getStatus()); + item.getItemProperty(PROP_TYPE).setValue(rollout.getActionType()); item.getItemProperty(SPUILabelDefinitions.VAR_TOTAL_TARGETS_COUNT_STATUS).setValue(totalTargetCountStatus); final Long groupCount = Long .valueOf((Integer) item.getItemProperty(SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS).getValue()); @@ -270,6 +282,7 @@ public class RolloutListGrid extends AbstractGrid { false); rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_APPROVAL_DECIDED_BY, String.class, null, false, false); + rolloutGridContainer.addContainerProperty(PROP_TYPE, ActionType.class, null, false, false); rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_APPROVAL_REMARK, String.class, null, false, false); rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_MODIFIED_DATE, String.class, null, false, @@ -307,6 +320,9 @@ public class RolloutListGrid extends AbstractGrid { getColumn(SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS).setMinimumWidth(40); getColumn(SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS).setMaximumWidth(60); + getColumn(PROP_TYPE).setMinimumWidth(45); + getColumn(PROP_TYPE).setMaximumWidth(45); + getColumn(VIRT_PROP_RUN).setMinimumWidth(25); getColumn(VIRT_PROP_RUN).setMaximumWidth(25); @@ -335,6 +351,7 @@ public class RolloutListGrid extends AbstractGrid { .setHeaderCaption(i18n.getMessage("header.distributionset")); getColumn(SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS).setHeaderCaption(i18n.getMessage("header.numberofgroups")); getColumn(SPUILabelDefinitions.VAR_TOTAL_TARGETS).setHeaderCaption(i18n.getMessage("header.total.targets")); + getColumn(PROP_TYPE).setHeaderCaption(i18n.getMessage("header.type")); getColumn(SPUILabelDefinitions.VAR_CREATED_DATE).setHeaderCaption(i18n.getMessage("header.createdDate")); getColumn(SPUILabelDefinitions.VAR_CREATED_USER).setHeaderCaption(i18n.getMessage("header.createdBy")); getColumn(SPUILabelDefinitions.VAR_MODIFIED_DATE).setHeaderCaption(i18n.getMessage("header.modifiedDate")); @@ -374,13 +391,14 @@ public class RolloutListGrid extends AbstractGrid { final List columnsToShowInOrder = Arrays.asList(ROLLOUT_RENDERER_DATA, SPUILabelDefinitions.VAR_DIST_NAME_VERSION, SPUILabelDefinitions.VAR_STATUS, SPUILabelDefinitions.VAR_TOTAL_TARGETS_COUNT_STATUS, SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS, - SPUILabelDefinitions.VAR_TOTAL_TARGETS, VIRT_PROP_APPROVE, VIRT_PROP_RUN, VIRT_PROP_PAUSE, + SPUILabelDefinitions.VAR_TOTAL_TARGETS, PROP_TYPE, VIRT_PROP_APPROVE, VIRT_PROP_RUN, VIRT_PROP_PAUSE, VIRT_PROP_UPDATE, VIRT_PROP_COPY, VIRT_PROP_DELETE, SPUILabelDefinitions.VAR_CREATED_DATE, SPUILabelDefinitions.VAR_CREATED_USER, SPUILabelDefinitions.VAR_MODIFIED_DATE, SPUILabelDefinitions.VAR_MODIFIED_BY, SPUILabelDefinitions.VAR_APPROVAL_DECIDED_BY, SPUILabelDefinitions.VAR_APPROVAL_REMARK, SPUILabelDefinitions.VAR_DESC); setColumns(columnsToShowInOrder.toArray()); + setCellStyleGenerator(alignGenerator); } @Override @@ -403,7 +421,7 @@ public class RolloutListGrid extends AbstractGrid { } @Override - protected void addColumnRenderes() { + protected void addColumnRenderers() { getColumn(SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS).setRenderer(new HtmlRenderer(), new TotalTargetGroupsConverter()); getColumn(SPUILabelDefinitions.VAR_TOTAL_TARGETS_COUNT_STATUS).setRenderer(new HtmlRenderer(), @@ -411,6 +429,9 @@ public class RolloutListGrid extends AbstractGrid { getColumn(SPUILabelDefinitions.VAR_STATUS).setRenderer(new HtmlLabelRenderer(), new RolloutStatusConverter()); + getColumn(PROP_TYPE).setRenderer(new HtmlLabelRenderer(), + new RolloutTypeConverter(this::createTypeLabelAdapter)); + final RolloutRenderer customObjectRenderer = new RolloutRenderer(RolloutRendererData.class); customObjectRenderer.addClickListener(this::onClickOfRolloutName); getColumn(ROLLOUT_RENDERER_DATA).setRenderer(customObjectRenderer); @@ -465,7 +486,7 @@ public class RolloutListGrid extends AbstractGrid { @Override public LazyQueryContainer getRawContainer() { - return (LazyQueryContainer) (getDecoratedContainer()).getWrappedContainer(); + return (LazyQueryContainer) getDecoratedContainer().getWrappedContainer(); } @Override @@ -550,7 +571,6 @@ public class RolloutListGrid extends AbstractGrid { if (RolloutStatus.PAUSED.equals(rolloutStatus)) { rolloutManagement.resumeRollout(rolloutId); uiNotification.displaySuccess(i18n.getMessage("message.rollout.resumed", rolloutName)); - return; } } @@ -608,7 +628,7 @@ public class RolloutListGrid extends AbstractGrid { } final Long runningActions = statusTotalCount.get(Status.RUNNING); String rolloutDetailsMessage = ""; - if ((scheduledActions > 0) || (runningActions > 0)) { + if (scheduledActions > 0 || runningActions > 0) { rolloutDetailsMessage = i18n.getMessage("message.delete.rollout.details", runningActions, scheduledActions); } @@ -716,6 +736,51 @@ public class RolloutListGrid extends AbstractGrid { } } + /** + * + * Converter to convert {@link RolloutStatus} to string. + * + */ + + class RolloutTypeConverter extends AbstractHtmlLabelConverter { + + private static final long serialVersionUID = 1L; + + RolloutTypeConverter(final LabelAdapter adapter) { + addAdapter(adapter); + } + + @Override + public Class getModelType() { + return ActionType.class; + } + + } + + private StatusFontIcon createTypeLabelAdapter(final ActionType actionType) { + if (ActionType.FORCED.equals(actionType)) { + return new StatusFontIcon(FontAwesome.BOLT, STATUS_ICON_FORCED, + i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_FORCED), + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); + } + if (ActionType.TIMEFORCED.equals(actionType)) { + return new StatusFontIcon(FontAwesome.HISTORY, STATUS_ICON_FORCED, + i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_TIME_FORCED), + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); + } + if (ActionType.SOFT.equals(actionType)) { + return new StatusFontIcon(FontAwesome.STEP_FORWARD, STATUS_ICON_SOFT, + i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_SOFT), + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); + } + if (ActionType.DOWNLOAD_ONLY.equals(actionType)) { + return new StatusFontIcon(FontAwesome.DOWNLOAD, STATUS_ICON_DOWNLOAD_ONLY, + i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_DOWNLOAD_ONLY), + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); + } + return null; + } + /** * Converter to convert {@link TotalTargetCountStatus} to formatted string * with status and count details. @@ -782,7 +847,7 @@ public class RolloutListGrid extends AbstractGrid { } } - private final void hideColumnsDueToInsufficientPermissions() { + private void hideColumnsDueToInsufficientPermissions() { final List modifiableColumnsList = getColumns().stream().map(Column::getPropertyId) .collect(Collectors.toList()); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgroup/RolloutGroupListGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgroup/RolloutGroupListGrid.java index 4327b4d33..c90d09ecc 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgroup/RolloutGroupListGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgroup/RolloutGroupListGrid.java @@ -233,7 +233,7 @@ public class RolloutGroupListGrid extends AbstractGrid { } @Override - protected void addColumnRenderes() { + protected void addColumnRenderers() { getColumn(SPUILabelDefinitions.VAR_STATUS).setRenderer(new HtmlLabelRenderer(), new RolloutGroupStatusConverter()); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgrouptargets/RolloutGroupTargetsListGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgrouptargets/RolloutGroupTargetsListGrid.java index b6882c74e..662db3373 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgrouptargets/RolloutGroupTargetsListGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgrouptargets/RolloutGroupTargetsListGrid.java @@ -12,6 +12,7 @@ import java.util.EnumMap; import java.util.Locale; import java.util.Map; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus; @@ -173,7 +174,7 @@ public class RolloutGroupTargetsListGrid extends AbstractGrid Action.ActionType.DOWNLOAD_ONLY.equals(group.getRollout().getActionType())) + .orElse(false); + + return isFinishedDownloadOnlyAssignment ? statusIconMap.get(Status.FINISHED) : statusIconMap.get(status); + } + private String getStatus() { final RolloutGroup rolloutGroup = rolloutUIState.getRolloutGroup().orElse(null); if (rolloutGroup != null && rolloutGroup.getStatus() == RolloutGroupStatus.READY) { 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 408a36427..2c34c3fde 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 @@ -177,6 +177,10 @@ public final class UIComponentIdProvider { * Action history table cancel Id. */ public static final String ACTION_DETAILS_SOFT_ID = "action.details.soft.group"; + /** + * Download Only Action Id. + */ + public static final String ACTION_DETAILS_DOWNLOAD_ONLY_ID = "action.details.downloadonly.group"; /** * Start type of rollout manual radio button */ @@ -256,7 +260,7 @@ public final class UIComponentIdProvider { /** * Action history table forced label Id. */ - public static final String ACTION_HISTORY_TABLE_FORCED_LABEL_ID = "action.history.table.forcedId"; + public static final String ACTION_HISTORY_TABLE_TYPE_LABEL_ID = "action.history.table.typeId"; /** * Action history table time-forced label Id. @@ -1145,6 +1149,11 @@ public final class UIComponentIdProvider { */ public static final String METADATA_POPUP_ID = "metadata.popup.id"; + /** + * Rollout popup id. + */ + public static final String ROLLOUT_POPUP_ID = "add.update.rollout.popup"; + /** * DistributionSet table details tab id in Distributions . */ diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java index 2efa408a5..05e874c9f 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java @@ -31,6 +31,8 @@ public final class UIMessageIdProvider { public static final String CAPTION_ACTION_SOFT = "label.action.soft"; + public static final String CAPTION_ACTION_DOWNLOAD_ONLY = "label.action.downloadonly"; + public static final String CAPTION_ACTION_TIME_FORCED = "label.action.time.forced"; public static final String CAPTION_ACTION_MESSAGES = "caption.action.messages"; @@ -129,6 +131,8 @@ public final class UIMessageIdProvider { public static final String TOOLTIP_SOFT_ITEM = "tooltip.soft.item"; + public static final String TOOLTIP_DOWNLOAD_ONLY_ITEM = "tooltip.downloadonly.item"; + public static final String TOOLTIP_FORCED_ITEM = "tooltip.forced.item"; public static final String TOOLTIP_TARGET_PIN = "tooltip.target.pin"; diff --git a/hawkbit-ui/src/main/resources/messages.properties b/hawkbit-ui/src/main/resources/messages.properties index 37bffcf5b..d138cddc3 100644 --- a/hawkbit-ui/src/main/resources/messages.properties +++ b/hawkbit-ui/src/main/resources/messages.properties @@ -165,7 +165,9 @@ label.active = Active label.action.id = Action Id label.no.tag = NO TAG label.action.forced = Forced +label.action.type = Type label.action.soft = Soft +label.action.downloadonly = Download Only label.action.time.forced = Time Forced label.dist.details.type = Type : label.dist.details.name = Name : @@ -218,6 +220,7 @@ label.cancelled = Cancelled label.cancelling = Canceling label.retrieved = Retrieved label.download = Downloading +label.downloaded = Downloaded label.unknown = Unknown label.target.id = Controller Id : label.target.ip = Controller IP : @@ -283,6 +286,7 @@ tooltip.status.overdue = Overdue tooltip.delete.module = Select and delete Software Module tooltip.forced.item=Device is supposed to install the update immediately tooltip.soft.item=Device can execute the update at any time, e.g. with user approval or according to its regular update time plan +tooltip.downloadonly.item=Device is supposed to only download the update and not install it tooltip.timeforced.item=Soft update which turns into a forced update after a specific time tooltip.timeforced.forced.in=Auto forcing in {0} tooltip.timeforced.forced.since=Auto forced since {0} @@ -409,6 +413,7 @@ message.forcequit.action = Force Quit.. message.forcequit.action.success = Action has been force quit successfully ! message.forcequit.action.failed = Force Quitting the action is not possible ! message.forcequit.action.confirm = Attention!\nForce quit should only be used when the assignment action is not working properly.\nForce quitting an action has no effect on the connected target. It is just resetting \nthe data stored on the SP update server. \nAre you absolutely sure you want to force quit this action? +message.downloadonly.action = DownloadOnly message.distribution.no.update = distribution {0} set is already assigned to targets and cannot be changed message.action.not.allowed = Action not allowed message.action.did.not.work = Action did not work. Please try again. @@ -570,6 +575,7 @@ header.name = Name header.vendor = Vendor header.version = Version header.description = Description +header.type = Type header.createdBy = Created By header.createdDate = Created Date header.modifiedBy = Modified By