From e2ca4cf840dc0dbbc2e7a37be66b401ff50d2499 Mon Sep 17 00:00:00 2001 From: Kai Zimmermann Date: Thu, 27 Jul 2017 17:28:56 +0200 Subject: [PATCH] Feature offline deployments (#563) * Repository support offline deployments. Signed-off-by: kaizimmerm * Add offline assignment to Management API. Signed-off-by: kaizimmerm * DsAssignmentStrategy introduced. Signed-off-by: kaizimmerm * Fixed JavaDoc. Signed-off-by: kaizimmerm * Readibility improved. Signed-off-by: kaizimmerm --- .../rest/api/MgmtDistributionSetRestApi.java | 7 +- .../mgmt/rest/api/MgmtTargetRestApi.java | 15 +- .../resource/MgmtDistributionSetResource.java | 21 +- .../rest/resource/MgmtTargetResource.java | 18 +- .../MgmtDistributionSetResourceTest.java | 43 +++- .../rest/resource/MgmtTargetResourceTest.java | 41 ++- .../repository/DeploymentManagement.java | 35 ++- .../jpa/AbstractDsAssignmentStrategy.java | 188 ++++++++++++++ .../jpa/JpaDeploymentManagement.java | 240 +++++++----------- .../jpa/OfflineDsAssignmentStrategy.java | 96 +++++++ .../jpa/OnlineDsAssignmentStrategy.java | 97 +++++++ .../RepositoryApplicationConfiguration.java | 14 +- .../repository/jpa/TargetRepository.java | 23 ++ .../specifications/TargetSpecifications.java | 13 + .../jpa/DeploymentManagementTest.java | 32 +++ 15 files changed, 693 insertions(+), 190 deletions(-) create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OfflineDsAssignmentStrategy.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OnlineDsAssignmentStrategy.java diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetRestApi.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetRestApi.java index fddfaf545..b4200494c 100644 --- a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetRestApi.java +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetRestApi.java @@ -219,6 +219,9 @@ public interface MgmtDistributionSetRestApi { * @param targetIds * the IDs of the target which should get assigned to the * distribution set given in the response body + * @param offline + * to true if update was executed offline, i.e. not + * managed by hawkBit. * @return status OK if the assignment of the targets was successful and a * complex return body which contains information about the assigned * targets and the already assigned targets counters @@ -227,7 +230,9 @@ public interface MgmtDistributionSetRestApi { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) ResponseEntity createAssignedTarget( - @PathVariable("distributionSetId") Long distributionSetId, List targetIds); + @PathVariable("distributionSetId") Long distributionSetId, + final List targetIds, + @RequestParam(value = "offline", required = false) boolean offline); /** * Gets a paged list of meta data for a distribution set. diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java index 10dde5c50..9d57afc36 100644 --- a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java @@ -15,6 +15,7 @@ import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentResponseBody; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssigment; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAttributes; @@ -245,6 +246,7 @@ public interface MgmtTargetRestApi { * * @param controllerId * the ID of the target to retrieve the assigned distribution + * * @return the assigned distribution set with status OK, if none is assigned * than {@code null} content (e.g. "{}") */ @@ -259,13 +261,20 @@ public interface MgmtTargetRestApi { * of the target to change * @param dsId * of the distributionset that is to be assigned - * @return http status + * @param offline + * to true if update was executed offline, i.e. not + * managed by hawkBit. + * + * @return status OK if the assignment of the targets was successful and a + * complex return body which contains information about the assigned + * targets and the already assigned targets counters */ @RequestMapping(method = RequestMethod.POST, value = "/{controllerId}/assignedDS", consumes = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) - ResponseEntity postAssignedDistributionSet(@PathVariable("controllerId") String controllerId, - MgmtDistributionSetAssigment dsId); + ResponseEntity postAssignedDistributionSet( + @PathVariable("controllerId") String controllerId, MgmtDistributionSetAssigment dsId, + @RequestParam(value = "offline", required = false) boolean offline); /** * Handles the GET request of retrieving the installed distribution set of diff --git a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java index 0cad26bef..ddf81abca 100644 --- a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java +++ b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java @@ -35,7 +35,6 @@ import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.model.DistributionSet; -import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; @@ -229,16 +228,22 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { @Override public ResponseEntity createAssignedTarget( @PathVariable("distributionSetId") final Long distributionSetId, - @RequestBody final List targetIds) { + @RequestBody final List assignments, + @RequestParam(value = "offline", required = false) final boolean offline) { - final DistributionSetAssignmentResult assignDistributionSet = this.deployManagament.assignDistributionSet( + if (offline) { + return ResponseEntity.ok(MgmtDistributionSetMapper + .toResponse(this.deployManagament.offlineAssignedDistributionSet(distributionSetId, assignments + .stream().map(MgmtTargetAssignmentRequestBody::getId).collect(Collectors.toList())))); + } + + return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(this.deployManagament.assignDistributionSet( distributionSetId, - targetIds.stream() - .map(t -> new TargetWithActionType(t.getId(), - MgmtRestModelMapper.convertActionType(t.getType()), t.getForcetime())) - .collect(Collectors.toList())); + assignments.stream() + .map(assignment -> new TargetWithActionType(assignment.getId(), + MgmtRestModelMapper.convertActionType(assignment.getType()), assignment.getForcetime())) + .collect(Collectors.toList())))); - return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(assignDistributionSet)); } @Override diff --git a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java index 2710d7afe..385810baf 100644 --- a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java +++ b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java @@ -21,6 +21,7 @@ import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentResponseBody; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssigment; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAttributes; @@ -253,16 +254,21 @@ public class MgmtTargetResource implements MgmtTargetRestApi { } @Override - public ResponseEntity postAssignedDistributionSet(@PathVariable("controllerId") final String controllerId, - @RequestBody final MgmtDistributionSetAssigment dsId) { + public ResponseEntity postAssignedDistributionSet( + @PathVariable("controllerId") final String controllerId, + @RequestBody final MgmtDistributionSetAssigment dsId, + @RequestParam(value = "offline", required = false) final boolean offline) { + + if (offline) { + return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse( + deploymentManagement.offlineAssignedDistributionSet(dsId.getId(), Arrays.asList(controllerId)))); + } findTargetWithExceptionIfNotFound(controllerId); final ActionType type = (dsId.getType() != null) ? MgmtRestModelMapper.convertActionType(dsId.getType()) : ActionType.FORCED; - this.deploymentManagement.assignDistributionSet(dsId.getId(), type, dsId.getForcetime(), - Arrays.asList(controllerId)); - - return ResponseEntity.ok().build(); + return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(deploymentManagement + .assignDistributionSet(dsId.getId(), type, dsId.getForcetime(), Arrays.asList(controllerId)))); } @Override diff --git a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java index ef2984bc7..b9e12c958 100644 --- a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java +++ b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java @@ -205,30 +205,49 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr @Test @Description("Ensures that multi target assignment through API is reflected by the repository.") public void assignMultipleTargetsToDistributionSet() throws Exception { - // prepare distribution set - final Set createDistributionSetsAlphabetical = createDistributionSetsAlphabetical(1); - final DistributionSet createdDs = createDistributionSetsAlphabetical.iterator().next(); - // prepare targets - final String[] knownTargetIds = new String[] { "1", "2", "3", "4", "5" }; + final DistributionSet createdDs = testdataFactory.createDistributionSet(); + final List targets = testdataFactory.createTargets(5); final JSONArray list = new JSONArray(); - for (final String targetId : knownTargetIds) { - testdataFactory.createTarget(targetId); - list.put(new JSONObject().put("id", Long.valueOf(targetId))); - } + targets.forEach(target -> list.put(new JSONObject().put("id", target.getControllerId()))); + // assign already one target to DS - assignDistributionSet(createdDs.getId(), knownTargetIds[0]); + assignDistributionSet(createdDs.getId(), targets.get(0).getControllerId()); mvc.perform(post( MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + createdDs.getId() + "/assignedTargets") .contentType(MediaType.APPLICATION_JSON).content(list.toString())) - .andExpect(status().isOk()).andExpect(jsonPath("$.assigned", equalTo(knownTargetIds.length - 1))) + .andExpect(status().isOk()).andExpect(jsonPath("$.assigned", equalTo(targets.size() - 1))) .andExpect(jsonPath("$.alreadyAssigned", equalTo(1))) - .andExpect(jsonPath("$.total", equalTo(knownTargetIds.length))); + .andExpect(jsonPath("$.total", equalTo(targets.size()))); assertThat(targetManagement.findTargetByAssignedDistributionSet(createdDs.getId(), PAGE).getContent()) .as("Five targets in repository have DS assigned").hasSize(5); } + @Test + @Description("Ensures that offline reported multi target assignment through API is reflected by the repository.") + public void offlineAssignmentOfMultipleTargetsToDistributionSet() throws Exception { + final DistributionSet createdDs = testdataFactory.createDistributionSet(); + final List targets = testdataFactory.createTargets(5); + final JSONArray list = new JSONArray(); + targets.forEach(target -> list.put(new JSONObject().put("id", target.getControllerId()))); + + // assign already one target to DS + assignDistributionSet(createdDs.getId(), targets.get(0).getControllerId()); + + mvc.perform(post(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + createdDs.getId() + + "/assignedTargets?offline=true").contentType(MediaType.APPLICATION_JSON).content(list.toString())) + .andExpect(status().isOk()).andExpect(jsonPath("$.assigned", equalTo(targets.size() - 1))) + .andExpect(jsonPath("$.alreadyAssigned", equalTo(1))) + .andExpect(jsonPath("$.total", equalTo(targets.size()))); + + assertThat(targetManagement.findTargetByAssignedDistributionSet(createdDs.getId(), PAGE).getContent()) + .as("Five targets in repository have DS assigned").hasSize(5); + + assertThat(targetManagement.findTargetByInstalledDistributionSet(createdDs.getId(), PAGE).getContent()) + .hasSize(4); + } + @Test @Description("Ensures that assigned targets of DS are returned as reflected by the repository.") public void getAssignedTargetsOfDistributionSet() throws Exception { diff --git a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java index 1e360d08c..191712807 100644 --- a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java +++ b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java @@ -47,6 +47,7 @@ import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.test.util.WithUser; import org.eclipse.hawkbit.rest.exception.MessageNotReadableException; import org.eclipse.hawkbit.rest.json.model.ExceptionInfo; @@ -1145,7 +1146,9 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + "/assignedDS") .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + .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); target = targetManagement.findTargetByControllerID(target.getControllerId()).get(); @@ -1153,7 +1156,41 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest // repeating DS assignment leads again to OK mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + "/assignedDS") .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("assigned", equalTo(0))).andExpect(jsonPath("alreadyAssigned", equalTo(1))) + .andExpect(jsonPath("total", equalTo(1))); + + // ...but does not change the target + assertThat(targetManagement.findTargetByControllerID(target.getControllerId()).get()).isEqualTo(target); + } + + @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.") + public void offlineAssignDistributionSetToTarget() throws Exception { + + Target target = testdataFactory.createTarget(); + final DistributionSet set = testdataFactory.createDistributionSet("one"); + + mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + + "/assignedDS?offline=true").content("{\"id\":" + set.getId() + "}") + .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); + assertThat(deploymentManagement.getInstalledDistributionSet(target.getControllerId()).get()).isEqualTo(set); + target = targetManagement.findTargetByControllerID(target.getControllerId()).get(); + assertThat(target.getUpdateStatus()).isEqualTo(TargetUpdateStatus.IN_SYNC); + + // repeating DS assignment leads again to OK + mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + + "/assignedDS?offline=true").content("{\"id\":" + set.getId() + "}") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("assigned", equalTo(0))).andExpect(jsonPath("alreadyAssigned", equalTo(1))) + .andExpect(jsonPath("total", equalTo(1))); // ...but does not change the target assertThat(targetManagement.findTargetByControllerID(target.getControllerId()).get()).isEqualTo(target); diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java index c943d77a1..046d9e954 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java @@ -14,6 +14,7 @@ import java.util.Optional; import javax.validation.constraints.NotNull; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; @@ -28,6 +29,7 @@ import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.hibernate.validator.constraints.NotEmpty; import org.springframework.data.domain.Page; @@ -69,7 +71,6 @@ public interface DeploymentManagement { DistributionSetAssignmentResult assignDistributionSet(@NotNull Long dsID, @NotNull ActionType actionType, long forcedTimestamp, @NotEmpty Collection controllerIDs); - /** * method assigns the {@link DistributionSet} to all {@link Target}s by * their IDs with a specific {@link ActionType} and {@code forcetime}. @@ -116,6 +117,38 @@ public interface DeploymentManagement { DistributionSetAssignmentResult assignDistributionSet(@NotNull Long dsID, @NotEmpty Collection targets, String actionMessage); + /** + * Method registers an "offline" assignment, i.e. adds a completed action + * for the given {@link DistributionSet} to the given {@link Target}s. + * + * The handling differs to hawkBit managed updates my means that:
+ * + *
    + *
  1. it ignores targets completely that are in + * {@link TargetUpdateStatus#PENDING}.
  2. + *
  3. it creates completed actions.
  4. + *
  5. sets both installed and assigned DS on the target and switches the + * status to {@link TargetUpdateStatus#IN_SYNC}.
  6. + *
  7. does not send a {@link TargetAssignDistributionSetEvent}.
  8. + *
+ * + * @param dsID + * the ID of the distribution set that was assigned + * @param controllerIDs + * a list of IDs of the targets that where assigned + * @return the assignment result + * + * @throws IncompleteDistributionSetException + * if mandatory {@link SoftwareModuleType} are not assigned as + * defined by the {@link DistributionSetType}. + * + * @throws EntityNotFoundException + * if either provided {@link DistributionSet} or {@link Target}s + * do not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) + DistributionSetAssignmentResult offlineAssignedDistributionSet(Long dsID, Collection controllerIDs); + /** * Cancels given {@link Action} for given {@link Target}. The method will * immediately add a {@link Status#CANCELED} status to the action. However, diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java new file mode 100644 index 000000000..66b50500a --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java @@ -0,0 +1,188 @@ +/** + * 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.repository.jpa; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.repository.RepositoryConstants; +import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.CancelTargetAssignmentEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; +import org.eclipse.hawkbit.repository.jpa.configuration.Constants; +import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; +import org.eclipse.hawkbit.repository.jpa.model.JpaAction; +import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus; +import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; +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.DistributionSet; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetWithActionType; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEventPublisher; + +/** + * {@link DistributionSet} to {@link Target} assignment strategy as utility for + * {@link JpaDeploymentManagement}. + * + */ +public abstract class AbstractDsAssignmentStrategy { + + protected final TargetRepository targetRepository; + protected final AfterTransactionCommitExecutor afterCommit; + protected final ApplicationEventPublisher eventPublisher; + protected final ApplicationContext applicationContext; + private final ActionRepository actionRepository; + private final ActionStatusRepository actionStatusRepository; + + AbstractDsAssignmentStrategy(final TargetRepository targetRepository, + final AfterTransactionCommitExecutor afterCommit, final ApplicationEventPublisher eventPublisher, + final ApplicationContext applicationContext, final ActionRepository actionRepository, + final ActionStatusRepository actionStatusRepository) { + this.targetRepository = targetRepository; + this.afterCommit = afterCommit; + this.eventPublisher = eventPublisher; + this.applicationContext = applicationContext; + this.actionRepository = actionRepository; + this.actionStatusRepository = actionStatusRepository; + } + + /** + * Find targets to be considered for assignment. + * + * @param controllerIDs + * as provided by repository caller + * @param distributionSetId + * to assign + * @return list of targets up to {@link Constants#MAX_ENTRIES_IN_STATEMENT} + */ + abstract List findTargetsForAssignment(final List controllerIDs, final long distributionSetId); + + /** + * Update status and DS fields of given target. + * + * @param distributionSet + * to set + * @param targetIds + * to change + * @param currentUser + * for auditing + */ + abstract void updateTargetStatus(final JpaDistributionSet distributionSet, final List> targetIds, + final String currentUser); + + /** + * Cancels actions that can be canceled (i.e. + * {@link DistributionSet#isRequiredMigrationStep() is false}) + * as a result of the new assignment and returns all {@link Target}s where + * such actions existed. + * + * @param targetIds + * to cancel actions for + * @return {@link Set} of {@link Target#getId()}s + */ + abstract Set findTargetIdsToCancel(List> targetIds); + + /** + * Handles event sending related to the assignment. + * + * @param targets + * to send events for + * @param targetIdsCancelList + * targets where an action was canceled + * @param controllerIdsToActions + * mapping of {@link Target#getControllerId()} to new + * {@link Action} that was created as part of the assignment. + */ + abstract void sendAssignmentEvents(final List targets, final Set targetIdsCancelList, + final Map controllerIdsToActions); + + protected void sendTargetAssignDistributionSetEvent(final Action action) { + afterCommit.afterCommit(() -> eventPublisher + .publishEvent(new TargetAssignDistributionSetEvent(action, applicationContext.getId()))); + } + + protected void sendTargetUpdatedEvent(final JpaTarget target) { + afterCommit.afterCommit( + () -> eventPublisher.publishEvent(new TargetUpdatedEvent(target, applicationContext.getId()))); + } + + /** + * Removes {@link Action}s that are no longer necessary and sends + * cancellations to the controller. + * + * @param targetsIds + * to override {@link Action}s + */ + protected List overrideObsoleteUpdateActions(final Collection targetsIds) { + + // Figure out if there are potential target/action combinations that + // need to be considered for cancellation + final List activeActions = actionRepository + .findByActiveAndTargetIdInAndActionStatusNotEqualToAndDistributionSetRequiredMigrationStep(targetsIds, + Action.Status.CANCELING); + + return activeActions.stream().map(action -> { + action.setStatus(Status.CANCELING); + + // document that the status has been retrieved + actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELING, System.currentTimeMillis(), + RepositoryConstants.SERVER_MESSAGE_PREFIX + "cancel obsolete action due to new update")); + actionRepository.save(action); + + cancelAssignDistributionSetEvent(action.getTarget(), action.getId()); + + return action.getTarget().getId(); + }).collect(Collectors.toList()); + + } + + /** + * Sends the {@link CancelTargetAssignmentEvent} for a specific target to + * the eventPublisher. + * + * @param target + * the Target which has been assigned to a distribution set + * @param actionId + * the action id of the assignment + */ + void cancelAssignDistributionSetEvent(final Target target, final Long actionId) { + afterCommit.afterCommit(() -> eventPublisher + .publishEvent(new CancelTargetAssignmentEvent(target, actionId, applicationContext.getId()))); + } + + JpaAction createTargetAction(final Map targetsWithActionMap, final JpaTarget target, + final JpaDistributionSet set) { + final JpaAction actionForTarget = new JpaAction(); + final TargetWithActionType targetWithActionType = targetsWithActionMap.get(target.getControllerId()); + actionForTarget.setActionType(targetWithActionType.getActionType()); + actionForTarget.setForcedTime(targetWithActionType.getForceTime()); + actionForTarget.setActive(true); + actionForTarget.setTarget(target); + actionForTarget.setDistributionSet(set); + return actionForTarget; + } + + JpaActionStatus createActionStatus(final JpaAction action, final String actionMessage) { + final JpaActionStatus actionStatus = new JpaActionStatus(); + actionStatus.setAction(action); + actionStatus.setOccurredAt(action.getCreatedAt()); + + if (actionMessage != null) { + actionStatus.addMessage(actionMessage); + } + + return actionStatus; + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java index 9d2e57d98..d01473989 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java @@ -31,8 +31,6 @@ import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; -import org.eclipse.hawkbit.repository.event.remote.entity.CancelTargetAssignmentEvent; -import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.ForceQuitActionNotAllowedException; @@ -47,7 +45,6 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; -import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; @@ -62,7 +59,6 @@ import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.ConcurrencyFailureException; @@ -99,51 +95,70 @@ public class JpaDeploymentManagement implements DeploymentManagement { */ private static final int ACTION_PAGE_LIMIT = 1000; - @Autowired - private EntityManager entityManager; + private final EntityManager entityManager; + private final ActionRepository actionRepository; + private final DistributionSetRepository distributionSetRepository; + private final TargetRepository targetRepository; + private final ActionStatusRepository actionStatusRepository; + private final TargetManagement targetManagement; + private final AuditorAware auditorProvider; + private final ApplicationEventPublisher eventPublisher; + private final ApplicationContext applicationContext; + private final AfterTransactionCommitExecutor afterCommit; + private final VirtualPropertyReplacer virtualPropertyReplacer; + private final PlatformTransactionManager txManager; + private final OnlineDsAssignmentStrategy onlineDsAssignmentStrategy; + private final OfflineDsAssignmentStrategy offlineDsAssignmentStrategy; - @Autowired - private ActionRepository actionRepository; + JpaDeploymentManagement(final EntityManager entityManager, final ActionRepository actionRepository, + final DistributionSetRepository distributionSetRepository, final TargetRepository targetRepository, + final ActionStatusRepository actionStatusRepository, final TargetManagement targetManagement, + final AuditorAware auditorProvider, final ApplicationEventPublisher eventPublisher, + final ApplicationContext applicationContext, final AfterTransactionCommitExecutor afterCommit, + final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager) { + this.entityManager = entityManager; + this.actionRepository = actionRepository; + this.distributionSetRepository = distributionSetRepository; + this.targetRepository = targetRepository; + this.actionStatusRepository = actionStatusRepository; + this.targetManagement = targetManagement; + this.auditorProvider = auditorProvider; + this.eventPublisher = eventPublisher; + this.applicationContext = applicationContext; + this.afterCommit = afterCommit; + this.virtualPropertyReplacer = virtualPropertyReplacer; + this.txManager = txManager; + onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisher, + applicationContext, actionRepository, actionStatusRepository); + offlineDsAssignmentStrategy = new OfflineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisher, + applicationContext, actionRepository, actionStatusRepository); + } - @Autowired - private DistributionSetRepository distributionSetRepository; - - @Autowired - private TargetRepository targetRepository; - - @Autowired - private ActionStatusRepository actionStatusRepository; - - @Autowired - private TargetManagement targetManagement; - - @Autowired - private AuditorAware auditorProvider; - - @Autowired - private ApplicationEventPublisher eventPublisher; - - @Autowired - private ApplicationContext applicationContext; - - @Autowired - private AfterTransactionCommitExecutor afterCommit; - - @Autowired - private VirtualPropertyReplacer virtualPropertyReplacer; - - @Autowired - private PlatformTransactionManager txManager; + @Override + @Transactional(isolation = Isolation.READ_COMMITTED) + @Retryable(include = { + ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) + public DistributionSetAssignmentResult offlineAssignedDistributionSet(final Long dsID, + final Collection controllerIDs) { + return assignDistributionSetToTargets(dsID, + controllerIDs.stream() + .map(controllerId -> new TargetWithActionType(controllerId, ActionType.FORCED, -1)) + .collect(Collectors.toList()), + null, offlineDsAssignmentStrategy); + } @Override @Transactional(isolation = Isolation.READ_COMMITTED) @Retryable(include = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public DistributionSetAssignmentResult assignDistributionSet(final Long dsID, final ActionType actionType, - final long forcedTimestamp, final Collection targetIDs) { + final long forcedTimestamp, final Collection controllerIDs) { - return assignDistributionSetToTargets(dsID, targetIDs.stream() - .map(t -> new TargetWithActionType(t, actionType, forcedTimestamp)).collect(Collectors.toList()), null); + return assignDistributionSetToTargets(dsID, + controllerIDs.stream() + .map(controllerId -> new TargetWithActionType(controllerId, actionType, forcedTimestamp)) + .collect(Collectors.toList()), + null, onlineDsAssignmentStrategy); } @@ -154,7 +169,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { public DistributionSetAssignmentResult assignDistributionSet(final Long dsID, final Collection targets) { - return assignDistributionSetToTargets(dsID, targets, null); + return assignDistributionSetToTargets(dsID, targets, null, onlineDsAssignmentStrategy); } @Override @@ -164,12 +179,22 @@ public class JpaDeploymentManagement implements DeploymentManagement { public DistributionSetAssignmentResult assignDistributionSet(final Long dsID, final Collection targets, final String actionMessage) { - return assignDistributionSetToTargets(dsID, targets, actionMessage); + return assignDistributionSetToTargets(dsID, targets, actionMessage, onlineDsAssignmentStrategy); } /** * method assigns the {@link DistributionSet} to all {@link Target}s by * their IDs with a specific {@link ActionType} and {@code forcetime}. + * + * + * In case the update was executed offline (i.e. not managed by hawkBit) the + * handling differs my means that:
+ * A. it ignores targets completely that are in + * {@link TargetUpdateStatus#PENDING}.
+ * B. it created completed actions.
+ * C. sets both installed and assigned DS on the target and switches the + * status to {@link TargetUpdateStatus#IN_SYNC}
+ * D. does not send a {@link TargetAssignDistributionSetEvent}.
* * @param dsID * the ID of the distribution set to assign @@ -177,6 +202,8 @@ public class JpaDeploymentManagement implements DeploymentManagement { * a list of all targets and their action type * @param actionMessage * an optional message to be written into the action status + * @param offline + * to true in offline case * @return the assignment result * * @throw IncompleteDistributionSetException if mandatory @@ -184,7 +211,8 @@ public class JpaDeploymentManagement implements DeploymentManagement { * {@link DistributionSetType}. */ private DistributionSetAssignmentResult assignDistributionSetToTargets(final Long dsID, - final Collection targetsWithActionType, final String actionMessage) { + final Collection targetsWithActionType, final String actionMessage, + final AbstractDsAssignmentStrategy assignmentStrategy) { final JpaDistributionSet set = distributionSetRepository.findOne(dsID); if (set == null) { @@ -209,10 +237,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { // maximum 1000 elements, so we need to split the entries here and // execute multiple statements we take the target only into account if // the requested operation is no duplicate of a previous one - final List targets = Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream() - .map(ids -> targetRepository - .findAll(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, set.getId()))) - .flatMap(List::stream).collect(Collectors.toList()); + final List targets = assignmentStrategy.findTargetsForAssignment(controllerIDs, set.getId()); if (targets.isEmpty()) { // detaching as it is not necessary to persist the set itself @@ -229,8 +254,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { // need to remember which one we have been switched to canceling state // because for targets which we have changed to canceling we don't want // to publish the new action update event. - final Set targetIdsCancellList = targetIds.stream().map(this::overrideObsoleteUpdateActions) - .flatMap(Collection::stream).collect(Collectors.toSet()); + final Set targetIdsCancellList = assignmentStrategy.findTargetIdsToCancel(targetIds); // cancel all scheduled actions which are in-active, these actions were // not active before and the manual assignment which has been done @@ -245,24 +269,26 @@ public class JpaDeploymentManagement implements DeploymentManagement { currentUser = null; } - targetIds.forEach(tIds -> targetRepository.setAssignedDistributionSetAndUpdateStatus(TargetUpdateStatus.PENDING, - set, System.currentTimeMillis(), currentUser, tIds)); + assignmentStrategy.updateTargetStatus(set, targetIds, currentUser); + final Map targetIdsToActions = targets.stream() - .map(t -> actionRepository.save(createTargetAction(targetsWithActionMap, t, set))) + .map(t -> actionRepository.save(assignmentStrategy.createTargetAction(targetsWithActionMap, t, set))) .collect(Collectors.toMap(a -> a.getTarget().getControllerId(), Function.identity())); // create initial action status when action is created so we remember // the initial running status because we will change the status // of the action itself and with this action status we have a nicer // action history. - targetIdsToActions.values().forEach(action -> setRunningActionStatus(action, actionMessage)); + actionStatusRepository.save(targetIdsToActions.values().stream() + .map(action -> assignmentStrategy.createActionStatus(action, actionMessage)) + .collect(Collectors.toList())); // detaching as it is not necessary to persist the set itself entityManager.detach(set); // detaching as the entity has been updated by the JPQL query above targets.forEach(entityManager::detach); - sendAssignmentEvents(targets, targetIdsCancellList, targetIdsToActions); + assignmentStrategy.sendAssignmentEvents(targets, targetIdsCancellList, targetIdsToActions); return new DistributionSetAssignmentResult( targets.stream().map(Target::getControllerId).collect(Collectors.toList()), targets.size(), @@ -270,77 +296,6 @@ public class JpaDeploymentManagement implements DeploymentManagement { targetManagement); } - private void sendAssignmentEvents(final List targets, final Set targetIdsCancellList, - final Map targetIdsToActions) { - - targets.forEach(target -> { - sendTargetUpdatedEvent(target); - if (targetIdsCancellList.contains(target.getId())) { - return; - } - - sendTargetAssignDistributionSetEvent(targetIdsToActions.get(target.getControllerId())); - }); - } - - private static JpaAction createTargetAction(final Map targetsWithActionMap, - final JpaTarget target, final JpaDistributionSet set) { - final JpaAction actionForTarget = new JpaAction(); - final TargetWithActionType targetWithActionType = targetsWithActionMap.get(target.getControllerId()); - actionForTarget.setActionType(targetWithActionType.getActionType()); - actionForTarget.setForcedTime(targetWithActionType.getForceTime()); - actionForTarget.setActive(true); - actionForTarget.setStatus(Status.RUNNING); - actionForTarget.setTarget(target); - actionForTarget.setDistributionSet(set); - return actionForTarget; - } - - private void sendTargetAssignDistributionSetEvent(final Action action) { - afterCommit.afterCommit(() -> eventPublisher - .publishEvent(new TargetAssignDistributionSetEvent(action, applicationContext.getId()))); - } - - private void sendTargetUpdatedEvent(final JpaTarget target) { - - // Update is not available in the object as the update was executed - // through JQL - target.setUpdateStatus(TargetUpdateStatus.PENDING); - - afterCommit.afterCommit( - () -> eventPublisher.publishEvent(new TargetUpdatedEvent(target, applicationContext.getId()))); - } - - /** - * Removes {@link Action}s that are no longer necessary and sends - * cancellations to the controller. - * - * @param targetsIds - * to override {@link Action}s - */ - private List overrideObsoleteUpdateActions(final Collection targetsIds) { - - // Figure out if there are potential target/action combinations that - // need to be considered for cancellation - final List activeActions = actionRepository - .findByActiveAndTargetIdInAndActionStatusNotEqualToAndDistributionSetRequiredMigrationStep(targetsIds, - Action.Status.CANCELING); - - return activeActions.stream().map(action -> { - action.setStatus(Status.CANCELING); - // document that the status has been retrieved - - actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELING, System.currentTimeMillis(), - RepositoryConstants.SERVER_MESSAGE_PREFIX + "cancel obsolete action due to new update")); - actionRepository.save(action); - - cancelAssignDistributionSetEvent(action.getTarget(), action.getId()); - - return action.getTarget().getId(); - }).collect(Collectors.toList()); - - } - @Override @Transactional(isolation = Isolation.READ_COMMITTED) @Retryable(include = { @@ -363,7 +318,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELING, System.currentTimeMillis(), RepositoryConstants.SERVER_MESSAGE_PREFIX + "manual cancelation requested")); final Action saveAction = actionRepository.save(action); - cancelAssignDistributionSetEvent(action.getTarget(), action.getId()); + onlineDsAssignmentStrategy.cancelAssignDistributionSetEvent(action.getTarget(), action.getId()); return saveAction; } else { @@ -371,20 +326,6 @@ public class JpaDeploymentManagement implements DeploymentManagement { } } - /** - * Sends the {@link CancelTargetAssignmentEvent} for a specific target to - * the eventPublisher. - * - * @param target - * the Target which has been assigned to a distribution set - * @param actionId - * the action id of the assignment - */ - private void cancelAssignDistributionSetEvent(final Target target, final Long actionId) { - afterCommit.afterCommit(() -> eventPublisher - .publishEvent(new CancelTargetAssignmentEvent(target, actionId, applicationContext.getId()))); - } - @Override @Transactional(isolation = Isolation.READ_COMMITTED) @Retryable(include = { @@ -471,14 +412,14 @@ public class JpaDeploymentManagement implements DeploymentManagement { } // check if we need to override running update actions - final List overrideObsoleteUpdateActions = overrideObsoleteUpdateActions( - Collections.singletonList(action.getTarget().getId())); + final List overrideObsoleteUpdateActions = onlineDsAssignmentStrategy + .overrideObsoleteUpdateActions(Collections.singletonList(action.getTarget().getId())); action.setActive(true); action.setStatus(Status.RUNNING); final JpaAction savedAction = actionRepository.save(action); - setRunningActionStatus(savedAction, null); + actionStatusRepository.save(onlineDsAssignmentStrategy.createActionStatus(savedAction, null)); target = (JpaTarget) entityManager.merge(savedAction.getTarget()); @@ -494,18 +435,6 @@ public class JpaDeploymentManagement implements DeploymentManagement { } } - private void setRunningActionStatus(final JpaAction action, final String actionMessage) { - final JpaActionStatus actionStatus = new JpaActionStatus(); - actionStatus.setAction(action); - actionStatus.setOccurredAt(action.getCreatedAt()); - actionStatus.setStatus(Status.RUNNING); - if (actionMessage != null) { - actionStatus.addMessage(actionMessage); - } - - actionStatusRepository.save(actionStatus); - } - private void setSkipActionStatus(final JpaAction action) { final JpaActionStatus actionStatus = new JpaActionStatus(); actionStatus.setAction(action); @@ -683,4 +612,5 @@ public class JpaDeploymentManagement implements DeploymentManagement { return distributionSetRepository.findInstalledAtTarget(controllerId); } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OfflineDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OfflineDsAssignmentStrategy.java new file mode 100644 index 000000000..671eecf44 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OfflineDsAssignmentStrategy.java @@ -0,0 +1,96 @@ +/** + * 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.repository.jpa; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.repository.RepositoryConstants; +import org.eclipse.hawkbit.repository.jpa.configuration.Constants; +import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; +import org.eclipse.hawkbit.repository.jpa.model.JpaAction; +import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus; +import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; +import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; +import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.repository.model.TargetWithActionType; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEventPublisher; + +import com.google.common.collect.Lists; + +/** + * AbstractDsAssignmentStrategy for offline assignments, i.e. not managed by + * hawkBit. + * + */ +public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { + + OfflineDsAssignmentStrategy(final TargetRepository targetRepository, + final AfterTransactionCommitExecutor afterCommit, final ApplicationEventPublisher eventPublisher, + final ApplicationContext applicationContext, final ActionRepository actionRepository, + final ActionStatusRepository actionStatusRepository) { + super(targetRepository, afterCommit, eventPublisher, applicationContext, actionRepository, + actionStatusRepository); + } + + @Override + void sendAssignmentEvents(final List targets, final Set targetIdsCancellList, + final Map targetIdsToActions) { + + targets.forEach(target -> { + target.setUpdateStatus(TargetUpdateStatus.IN_SYNC); + sendTargetUpdatedEvent(target); + }); + } + + @Override + public List findTargetsForAssignment(final List controllerIDs, final long setId) { + return Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream() + .map(ids -> targetRepository.findAll(SpecificationsBuilder.combineWithAnd( + Arrays.asList(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, setId), + TargetSpecifications.notEqualToTargetUpdateStatus(TargetUpdateStatus.PENDING))))) + .flatMap(List::stream).collect(Collectors.toList()); + } + + @Override + public Set findTargetIdsToCancel(final List> targetIds) { + return Collections.emptySet(); + } + + @Override + void updateTargetStatus(final JpaDistributionSet set, final List> targetIds, final String currentUser) { + targetIds.forEach(tIds -> targetRepository.setAssignedAndInstalledDistributionSetAndUpdateStatus( + TargetUpdateStatus.IN_SYNC, set, System.currentTimeMillis(), currentUser, tIds)); + } + + @Override + protected JpaAction createTargetAction(final Map targetsWithActionMap, + final JpaTarget target, final JpaDistributionSet set) { + final JpaAction result = super.createTargetAction(targetsWithActionMap, target, set); + result.setStatus(Status.FINISHED); + return result; + } + + @Override + protected JpaActionStatus createActionStatus(final JpaAction action, final String actionMessage) { + final JpaActionStatus result = super.createActionStatus(action, actionMessage); + result.setStatus(Status.FINISHED); + result.addMessage(RepositoryConstants.SERVER_MESSAGE_PREFIX + "Action reported as offline deployment"); + return result; + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OnlineDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OnlineDsAssignmentStrategy.java new file mode 100644 index 000000000..3f6b0d3d0 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OnlineDsAssignmentStrategy.java @@ -0,0 +1,97 @@ +/** + * 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.repository.jpa; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.repository.jpa.configuration.Constants; +import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; +import org.eclipse.hawkbit.repository.jpa.model.JpaAction; +import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus; +import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; +import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.repository.model.TargetWithActionType; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEventPublisher; + +import com.google.common.collect.Lists; + +/** + * AbstractDsAssignmentStrategy for online assignments, i.e. managed by hawkBit. + * + */ +public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { + + OnlineDsAssignmentStrategy(final TargetRepository targetRepository, + final AfterTransactionCommitExecutor afterCommit, final ApplicationEventPublisher eventPublisher, + final ApplicationContext applicationContext, final ActionRepository actionRepository, + final ActionStatusRepository actionStatusRepository) { + super(targetRepository, afterCommit, eventPublisher, applicationContext, actionRepository, + actionStatusRepository); + } + + @Override + void sendAssignmentEvents(final List targets, final Set targetIdsCancellList, + final Map targetIdsToActions) { + + targets.forEach(target -> { + target.setUpdateStatus(TargetUpdateStatus.PENDING); + sendTargetUpdatedEvent(target); + if (targetIdsCancellList.contains(target.getId())) { + return; + } + + sendTargetAssignDistributionSetEvent(targetIdsToActions.get(target.getControllerId())); + }); + } + + @Override + List findTargetsForAssignment(final List controllerIDs, final long setId) { + return Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream() + .map(ids -> targetRepository + .findAll(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, setId))) + .flatMap(List::stream).collect(Collectors.toList()); + } + + @Override + Set findTargetIdsToCancel(final List> targetIds) { + return targetIds.stream().map(this::overrideObsoleteUpdateActions).flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + @Override + void updateTargetStatus(final JpaDistributionSet set, final List> targetIds, final String currentUser) { + targetIds.forEach(tIds -> targetRepository.setAssignedDistributionSetAndUpdateStatus(TargetUpdateStatus.PENDING, + set, System.currentTimeMillis(), currentUser, tIds)); + + } + + @Override + JpaAction createTargetAction(final Map targetsWithActionMap, final JpaTarget target, + final JpaDistributionSet set) { + final JpaAction result = super.createTargetAction(targetsWithActionMap, target, set); + result.setStatus(Status.RUNNING); + return result; + } + + @Override + JpaActionStatus createActionStatus(final JpaAction action, final String actionMessage) { + final JpaActionStatus result = super.createActionStatus(action, actionMessage); + result.setStatus(Status.RUNNING); + return result; + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index cae7ac600..ecdb65b3d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -50,6 +50,7 @@ import org.eclipse.hawkbit.repository.jpa.builder.JpaSoftwareModuleBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryBuilder; import org.eclipse.hawkbit.repository.jpa.configuration.MultiTenantJpaTransactionManager; import org.eclipse.hawkbit.repository.jpa.event.JpaEventEntityManager; +import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; import org.eclipse.hawkbit.repository.jpa.model.helper.AfterTransactionCommitExecutorHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.EntityInterceptorHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder; @@ -87,6 +88,7 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.PropertySource; +import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.integration.support.locks.LockRegistry; @@ -496,8 +498,16 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { */ @Bean @ConditionalOnMissingBean - DeploymentManagement deploymentManagement() { - return new JpaDeploymentManagement(); + DeploymentManagement deploymentManagement(final EntityManager entityManager, + final ActionRepository actionRepository, final DistributionSetRepository distributionSetRepository, + final TargetRepository targetRepository, final ActionStatusRepository actionStatusRepository, + final TargetManagement targetManagement, final AuditorAware auditorProvider, + final ApplicationEventPublisher eventPublisher, final ApplicationContext applicationContext, + final AfterTransactionCommitExecutor afterCommit, final VirtualPropertyReplacer virtualPropertyReplacer, + final PlatformTransactionManager txManager) { + return new JpaDeploymentManagement(entityManager, actionRepository, distributionSetRepository, targetRepository, + actionStatusRepository, targetManagement, auditorProvider, eventPublisher, applicationContext, + afterCommit, virtualPropertyReplacer, txManager); } /** diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java index 722daa4f8..c994e67bf 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java @@ -58,6 +58,29 @@ public interface TargetRepository extends BaseEntityRepository, @Param("set") JpaDistributionSet set, @Param("lastModifiedAt") Long modifiedAt, @Param("lastModifiedBy") String modifiedBy, @Param("targets") Collection targets); + /** + * Sets {@link JpaTarget#getAssignedDistributionSet()}, + * {@link JpaTarget#getInstalledDistributionSet()} and + * {@link JpaTarget#getInstallationDate()} + * + * @param set + * to use + * @param status + * to set + * @param modifiedAt + * current time + * @param modifiedBy + * current auditor + * @param targets + * to update + */ + @Modifying + @Transactional + @Query("UPDATE JpaTarget t SET t.assignedDistributionSet = :set, t.installedDistributionSet = :set, t.installationDate = :lastModifiedAt, t.lastModifiedAt = :lastModifiedAt, t.lastModifiedBy = :lastModifiedBy, t.updateStatus = :status WHERE t.id IN :targets") + void setAssignedAndInstalledDistributionSetAndUpdateStatus(@Param("status") TargetUpdateStatus status, + @Param("set") JpaDistributionSet set, @Param("lastModifiedAt") Long modifiedAt, + @Param("lastModifiedBy") String modifiedBy, @Param("targets") Collection targets); + /** * Loads {@link Target} by given ID. * diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java index c67b89921..518e324db 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java @@ -97,6 +97,19 @@ public final class TargetSpecifications { return (targetRoot, query, cb) -> targetRoot.get(JpaTarget_.updateStatus).in(updateStatus); } + /** + * {@link Specification} for retrieving {@link Target}s by "not equal to + * given {@link TargetUpdateStatus}". + * + * @param updateStatus + * to be filtered on + * + * @return the {@link Target} {@link Specification} + */ + public static Specification notEqualToTargetUpdateStatus(final TargetUpdateStatus updateStatus) { + return (targetRoot, query, cb) -> cb.not(cb.equal(targetRoot.get(JpaTarget_.updateStatus), updateStatus)); + } + /** * {@link Specification} for retrieving {@link Target}s that are overdue. A * target is overdue if it did not respond during the configured 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 6fd8ff33b..ca798dec0 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 @@ -428,6 +428,38 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { return action; } + @Test + @Description("Simple offline deployment of a distribution set to a list of targets. Verifies that offline assigment " + + "is correctly executed for targets that do not have a running update already. Those are ignored.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 20), + @Expect(type = TargetUpdatedEvent.class, count = 20), @Expect(type = ActionCreatedEvent.class, count = 20), + @Expect(type = DistributionSetCreatedEvent.class, count = 2), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 10) }) + public void assignedDistributionSet() { + final List controllerIds = testdataFactory.createTargets(10).stream().map(Target::getControllerId) + .collect(Collectors.toList()); + final List onlineAssignedTargets = testdataFactory.createTargets(10, "2"); + controllerIds.addAll(onlineAssignedTargets.stream().map(Target::getControllerId).collect(Collectors.toList())); + + final DistributionSet ds = testdataFactory.createDistributionSet(); + assignDistributionSet(testdataFactory.createDistributionSet("2"), onlineAssignedTargets); + + final long current = System.currentTimeMillis(); + final List targets = deploymentManagement.offlineAssignedDistributionSet(ds.getId(), controllerIds) + .getAssignedEntity(); + assertThat(actionRepository.count()).isEqualTo(20); + + assertThat(targetManagement.findTargetByInstalledDistributionSet(ds.getId(), PAGE).getContent()) + .containsAll(targets).hasSize(10) + .containsAll(targetManagement.findTargetByAssignedDistributionSet(ds.getId(), PAGE)) + .as("InstallationDate set").allMatch(target -> target.getInstallationDate() >= current) + .as("TargetUpdateStatus IN_SYNC") + .allMatch(target -> TargetUpdateStatus.IN_SYNC.equals(target.getUpdateStatus())) + .as("InstallationDate equal to LastModifiedAt") + .allMatch(target -> target.getLastModifiedAt().equals(target.getInstallationDate())); + } + /** * test a simple deployment by calling the * {@link TargetRepository#assignDistributionSet(DistributionSet, Iterable)}