diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java index 843a8fdf4..52f3801ae 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java @@ -162,6 +162,7 @@ public class SecurityManagedConfiguration { private static final String[] DDI_ANT_MATCHERS = { DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}", DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/deploymentBase/**", + DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/installedBase/**", DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/cancelAction/**", DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/configData", DdiRestConstants.BASE_V1_REQUEST_MAPPING diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java index de8708e8a..0a950b1bf 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java @@ -21,11 +21,11 @@ import javax.validation.constraints.NotNull; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; +import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException; -import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionStatus; @@ -499,4 +499,13 @@ public interface ControllerManagement { */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) void deleteExistingTarget(@NotEmpty String controllerId); + + /** + * Finds an {@link Action} based on the target that it's assigned to + * + * @param controllerId + * of the target the action is assigned to + */ + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + Optional getInstalledActionByTarget(@NotEmpty String controllerId); } 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 41589d6d0..ae1e86ba6 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 @@ -189,6 +189,18 @@ public interface ActionRepository extends BaseEntityRepository, @Query("Select a from JpaAction a join a.distributionSet ds join ds.modules modul where a.target.controllerId = :target and modul.id = :module order by a.id desc") List findActionByTargetAndSoftwareModule(@Param("target") String targetId, @Param("module") Long moduleId); + /** + * Retrieves latest {@link Action} for given target and {@link DistributionSet}. + * + * @param targetId + * to search for + * @param dsId + * to search for + * @return action if there is one with assigned target and assigned + * {@link DistributionSet}. + */ + Optional findFirstByTargetIdAndDistributionSetId(@Param("target") long targetId, @Param("ds") Long dsId); + /** * Retrieves all {@link Action}s which are referring the given * {@link DistributionSet} and {@link Target}. 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 66ea1af2d..0f76e467a 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 @@ -1056,6 +1056,20 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont return actionRepository.findByExternalRef(externalRef); } + @Override + public Optional getInstalledActionByTarget(final String controllerId) { + final JpaTarget jpaTarget = targetRepository.findOne(TargetSpecifications.hasControllerId(controllerId)) + .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); + + final JpaDistributionSet installedDistributionSet = jpaTarget.getInstalledDistributionSet(); + if (null != installedDistributionSet) { + return actionRepository.findFirstByTargetIdAndDistributionSetId(jpaTarget.getId(), + installedDistributionSet.getId()); + } else { + return Optional.empty(); + } + } + private void cancelAssignDistributionSetEvent(final JpaTarget target, final Long actionId) { afterCommit.afterCommit(() -> eventPublisherHolder.getEventPublisher().publishEvent( new CancelTargetAssignmentEvent(target, actionId, eventPublisherHolder.getApplicationId()))); diff --git a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java index 90e0b26df..bbedca42d 100644 --- a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java +++ b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java @@ -23,6 +23,11 @@ public final class DdiRestConstants { */ public static final String DEPLOYMENT_BASE_ACTION = "deploymentBase"; + /** + * Installed action resources. + */ + public static final String INSTALLED_BASE_ACTION = "installedBase"; + /** * Cancel action resources. */ diff --git a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java index 619ed1849..f7da399f7 100644 --- a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java +++ b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java @@ -62,8 +62,7 @@ public interface DdiRootControllerRestApi { * of the request * @param controllerId * of the target that matches to controller id - * @param request - * the HTTP request injected by spring + * * @return the response */ @GetMapping(value = "/{controllerId}", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, @@ -83,10 +82,6 @@ public interface DdiRootControllerRestApi { * of the parent software module * @param fileName * of the related local artifact - * @param response - * of the servlet - * @param request - * from the client * * @return response of the servlet which in case of success is status code * {@link HttpStatus#OK} or in case of partial download @@ -109,10 +104,6 @@ public interface DdiRootControllerRestApi { * of the parent software module * @param fileName * of the related local artifact - * @param response - * of the servlet - * @param request - * the HTTP request injected by spring * * @return {@link ResponseEntity} with status {@link HttpStatus#OK} if * successful @@ -144,14 +135,16 @@ public interface DdiRootControllerRestApi { * resource utilization by controllers, maximum number of * messages that are retrieved from database is limited by * {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}. - * actionHistoryMessageCount less then zero, retrieves the - * maximum allowed number of action status messages from history; - * actionHistoryMessageCount equal to zero, does not retrieve any - * message; and actionHistoryMessageCount greater then zero, - * retrieves the specified number of messages, limited by maximum - * allowed number. - * @param request - * the HTTP request injected by spring + * + * actionHistoryMessageCount less than zero: retrieves the maximum + * allowed number of action status messages from history; + * + * actionHistoryMessageCount equal to zero: does not retrieve any + * message; + * + * actionHistoryMessageCount greater than zero: retrieves the + * specified number of messages, limited by maximum allowed number. + * * @return the response */ @GetMapping(value = "/{controllerId}/" + DdiRestConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}", produces = { @@ -173,8 +166,6 @@ public interface DdiRootControllerRestApi { * of the target that matches to controller id * @param actionId * of the action we have feedback for - * @param request - * the HTTP request injected by spring * * @return the response */ @@ -194,8 +185,6 @@ public interface DdiRootControllerRestApi { * as body * @param controllerId * to provide data for - * @param request - * the HTTP request injected by spring * * @return status of the request */ @@ -213,8 +202,6 @@ public interface DdiRootControllerRestApi { * ID of the calling target * @param actionId * of the action - * @param request - * the HTTP request injected by spring * * @return the {@link DdiCancel} response */ @@ -228,20 +215,17 @@ public interface DdiRootControllerRestApi { * RequestMethod.POST method receiving the {@link DdiActionFeedback} from * the target. * - * @param tenant - * of the client * @param feedback * the {@link DdiActionFeedback} from the target. + * @param tenant + * of the client * @param controllerId * the ID of the calling target * @param actionId * of the action we have feedback for - * @param request - * the HTTP request injected by spring * * @return the {@link DdiActionFeedback} response */ - @PostMapping(value = "/{controllerId}/" + DdiRestConstants.CANCEL_ACTION + "/{actionId}/" + DdiRestConstants.FEEDBACK, consumes = { MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR }) @@ -250,4 +234,40 @@ public interface DdiRootControllerRestApi { @PathVariable("controllerId") @NotEmpty final String controllerId, @PathVariable("actionId") @NotEmpty final Long actionId); + /** + * Resource for installed distribution set to retrieve the last successfully + * finished action. + * + * @param tenant + * of the request + * @param controllerId + * of the target + * @param actionId + * of the {@link DdiDeploymentBase} that matches to installed action. + * @param actionHistoryMessageCount + * specifies the number of messages to be returned from action + * history. Regardless of the passed value, in order to restrict + * resource utilization by controllers, maximum number of messages + * that are retrieved from database is limited by + * {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}. + * + * actionHistoryMessageCount less than zero: retrieves the maximum + * allowed number of action status messages from history; + * + * actionHistoryMessageCount equal to zero: does not retrieve any + * message; + * + * actionHistoryMessageCount greater than zero: retrieves the + * specified number of messages, limited by maximum allowed number. + * + * @return the {@link DdiDeploymentBase}. The response is of same format as for + * the /deploymentBase resource. + */ + @GetMapping(value = "/{controllerId}/" + DdiRestConstants.INSTALLED_BASE_ACTION + "/{actionId}", produces = { + MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR }) + ResponseEntity getControllerInstalledAction(@PathVariable("tenant") final String tenant, + @PathVariable("controllerId") @NotEmpty final String controllerId, + @PathVariable("actionId") @NotEmpty final Long actionId, + @RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount); + } diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java b/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java index 3635cebb6..228d9c5c3 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java @@ -108,17 +108,17 @@ public final class DataConversionHelper { } - static DdiControllerBase fromTarget(final Target target, final Action action, - final String defaultControllerPollTime, final TenantAware tenantAware) { + public static DdiControllerBase fromTarget(final Target target, final Action installedAction, + final Action activeAction, final String defaultControllerPollTime, final TenantAware tenantAware) { final DdiControllerBase result = new DdiControllerBase( new DdiConfig(new DdiPolling(defaultControllerPollTime))); - if (action != null) { - if (action.isCancelingOrCanceled()) { + if (activeAction != null) { + if (activeAction.isCancelingOrCanceled()) { result.add(WebMvcLinkBuilder .linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant()) .getControllerCancelAction(tenantAware.getCurrentTenant(), target.getControllerId(), - action.getId())) + activeAction.getId())) .withRel(DdiRestConstants.CANCEL_ACTION)); } else { // we need to add the hashcode here of the actionWithStatus @@ -129,11 +129,20 @@ public final class DataConversionHelper { result.add(WebMvcLinkBuilder .linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant()) .getControllerBasedeploymentAction(tenantAware.getCurrentTenant(), - target.getControllerId(), action.getId(), calculateEtag(action), null)) + target.getControllerId(), activeAction.getId(), calculateEtag(activeAction), null)) .withRel(DdiRestConstants.DEPLOYMENT_BASE_ACTION)); } } + if (installedAction != null && !installedAction.isActive()) { + result.add( + WebMvcLinkBuilder + .linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant()) + .getControllerInstalledAction(tenantAware.getCurrentTenant(), + target.getControllerId(), installedAction.getId(), null)) + .withRel(DdiRestConstants.INSTALLED_BASE_ACTION)); + } + if (target.isRequestControllerAttributes()) { result.add(WebMvcLinkBuilder .linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant()) diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java b/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java index 23cef1d4c..2a7f6ccd1 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java @@ -130,8 +130,7 @@ public class DdiRootController implements DdiRootControllerRestApi { @PathVariable("softwareModuleId") final Long softwareModuleId) { LOG.debug("getSoftwareModulesArtifacts({})", controllerId); - final Target target = controllerManagement.getByControllerId(controllerId) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); + final Target target = findTargetWithExceptionIfNotFound(controllerId); final SoftwareModule softwareModule = controllerManagement.getSoftwareModule(softwareModuleId) .orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, softwareModuleId)); @@ -149,14 +148,16 @@ public class DdiRootController implements DdiRootControllerRestApi { final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, IpUtil .getClientIpFromRequest(requestResponseContextHolder.getHttpServletRequest(), securityProperties)); - final Action action = controllerManagement.findActiveActionWithHighestWeight(controllerId).orElse(null); + final Action activeAction = controllerManagement.findActiveActionWithHighestWeight(controllerId).orElse(null); - checkAndCancelExpiredAction(action); + final Action installedAction = controllerManagement.getInstalledActionByTarget(controllerId).orElse(null); + + checkAndCancelExpiredAction(activeAction); return new ResponseEntity<>( - DataConversionHelper.fromTarget(target, action, - action == null ? controllerManagement.getPollingTime() - : controllerManagement.getPollingTimeForAction(action.getId()), + DataConversionHelper.fromTarget(target, installedAction, activeAction, + activeAction == null ? controllerManagement.getPollingTime() + : controllerManagement.getPollingTimeForAction(activeAction.getId()), tenantAware), HttpStatus.OK); } @@ -168,8 +169,7 @@ public class DdiRootController implements DdiRootControllerRestApi { @PathVariable("fileName") final String fileName) { final ResponseEntity result; - final Target target = controllerManagement.getByControllerId(controllerId) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); + final Target target = findTargetWithExceptionIfNotFound(controllerId); final SoftwareModule module = controllerManagement.getSoftwareModule(softwareModuleId) .orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, softwareModuleId)); @@ -239,8 +239,7 @@ public class DdiRootController implements DdiRootControllerRestApi { @PathVariable("controllerId") final String controllerId, @PathVariable("softwareModuleId") final Long softwareModuleId, @PathVariable("fileName") final String fileName) { - final Target target = controllerManagement.getByControllerId(controllerId) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); + final Target target = findTargetWithExceptionIfNotFound(controllerId); final SoftwareModule module = controllerManagement.getSoftwareModule(softwareModuleId) .orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, softwareModuleId)); @@ -275,38 +274,15 @@ public class DdiRootController implements DdiRootControllerRestApi { @RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount) { LOG.debug("getControllerBasedeploymentAction({},{})", controllerId, resource); - final Target target = controllerManagement.getByControllerId(controllerId) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); - + final Target target = findTargetWithExceptionIfNotFound(controllerId); final Action action = findActionWithExceptionIfNotFound(actionId); - if (!action.getTarget().getId().equals(target.getId())) { - LOG.warn(GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET, action.getId(), target.getId()); - return ResponseEntity.notFound().build(); - } + verifyActionAssignedToTarget(target, action); checkAndCancelExpiredAction(action); if (!action.isCancelingOrCanceled()) { - final List chunks = DataConversionHelper.createChunks(target, action, artifactUrlHandler, - systemManagement, - new ServletServerHttpRequest(requestResponseContextHolder.getHttpServletRequest()), - controllerManagement); - - final List actionHistoryMsgs = controllerManagement.getActionHistoryMessages(action.getId(), - actionHistoryMessageCount == null ? Integer.parseInt(DdiRestConstants.NO_ACTION_HISTORY) - : actionHistoryMessageCount); - - final DdiActionHistory actionHistory = actionHistoryMsgs.isEmpty() ? null - : new DdiActionHistory(action.getStatus().name(), actionHistoryMsgs); - - final HandlingType downloadType = calculateDownloadType(action); - final HandlingType updateType = calculateUpdateType(action, downloadType); - - final DdiMaintenanceWindowStatus maintenanceWindow = calculateMaintenanceWindow(action); - - final DdiDeploymentBase base = new DdiDeploymentBase(Long.toString(action.getId()), - new DdiDeployment(downloadType, updateType, chunks, maintenanceWindow), actionHistory); + final DdiDeploymentBase base = generateDdiDeploymentBase(target, action, actionHistoryMessageCount); LOG.debug("Found an active UpdateAction for target {}. returning deployment: {}", controllerId, base); @@ -349,14 +325,9 @@ public class DdiRootController implements DdiRootControllerRestApi { @PathVariable("actionId") @NotEmpty final Long actionId) { LOG.debug("provideBasedeploymentActionFeedback for target [{},{}]: {}", controllerId, actionId, feedback); - final Target target = controllerManagement.getByControllerId(controllerId) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); - + final Target target = findTargetWithExceptionIfNotFound(controllerId); final Action action = findActionWithExceptionIfNotFound(actionId); - if (!action.getTarget().getId().equals(target.getId())) { - LOG.warn(GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET, action.getId(), target.getId()); - return ResponseEntity.notFound().build(); - } + verifyActionAssignedToTarget(target, action); if (!action.isActive()) { LOG.warn("Updating action {} with feedback {} not possible since action not active anymore.", @@ -461,14 +432,9 @@ public class DdiRootController implements DdiRootControllerRestApi { @PathVariable("actionId") @NotEmpty final Long actionId) { LOG.debug("getControllerCancelAction({})", controllerId); - final Target target = controllerManagement.getByControllerId(controllerId) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); - + final Target target = findTargetWithExceptionIfNotFound(controllerId); final Action action = findActionWithExceptionIfNotFound(actionId); - if (!action.getTarget().getId().equals(target.getId())) { - LOG.warn(GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET, action.getId(), target.getId()); - return ResponseEntity.notFound().build(); - } + verifyActionAssignedToTarget(target, action); if (action.isCancelingOrCanceled()) { final DdiCancel cancel = new DdiCancel(String.valueOf(action.getId()), @@ -492,20 +458,58 @@ public class DdiRootController implements DdiRootControllerRestApi { @PathVariable("actionId") @NotEmpty final Long actionId) { LOG.debug("provideCancelActionFeedback for target [{}]: {}", controllerId, feedback); - final Target target = controllerManagement.getByControllerId(controllerId) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); - + final Target target = findTargetWithExceptionIfNotFound(controllerId); final Action action = findActionWithExceptionIfNotFound(actionId); - if (!action.getTarget().getId().equals(target.getId())) { - LOG.warn(GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET, action.getId(), target.getId()); - return ResponseEntity.notFound().build(); - } + verifyActionAssignedToTarget(target, action); controllerManagement .addCancelActionStatus(generateActionCancelStatus(feedback, target, actionId, entityFactory)); return ResponseEntity.ok().build(); } + @Override + public ResponseEntity getControllerInstalledAction(@PathVariable("tenant") final String tenant, + @PathVariable("controllerId") final String controllerId, @PathVariable("actionId") final Long actionId, + @RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount) { + LOG.debug("getControllerInstalledAction({})", controllerId); + + final Target target = findTargetWithExceptionIfNotFound(controllerId); + final Action action = findActionWithExceptionIfNotFound(actionId); + verifyActionAssignedToTarget(target, action); + + if (action.isActive() || action.isCancelingOrCanceled()) { + return ResponseEntity.notFound().build(); + } + + final DdiDeploymentBase base = generateDdiDeploymentBase(target, action, actionHistoryMessageCount); + + LOG.debug("Found an installed UpdateAction for target {}. returning deployment: {}", controllerId, base); + return new ResponseEntity<>(base, HttpStatus.OK); + + } + + private DdiDeploymentBase generateDdiDeploymentBase(Target target, Action action, + Integer actionHistoryMessageCount) { + final List chunks = DataConversionHelper.createChunks(target, action, artifactUrlHandler, + systemManagement, new ServletServerHttpRequest(requestResponseContextHolder.getHttpServletRequest()), + controllerManagement); + + final List actionHistoryMsgs = controllerManagement.getActionHistoryMessages(action.getId(), + actionHistoryMessageCount == null ? Integer.parseInt(DdiRestConstants.NO_ACTION_HISTORY) + : actionHistoryMessageCount); + + final DdiActionHistory actionHistory = actionHistoryMsgs.isEmpty() ? null + : new DdiActionHistory(action.getStatus().name(), actionHistoryMsgs); + + final HandlingType downloadType = calculateDownloadType(action); + final HandlingType updateType = calculateUpdateType(action, downloadType); + + final DdiMaintenanceWindowStatus maintenanceWindow = calculateMaintenanceWindow(action); + + return new DdiDeploymentBase(Long.toString(action.getId()), + new DdiDeployment(downloadType, updateType, chunks, maintenanceWindow), actionHistory); + } + private static ActionStatusCreate generateActionCancelStatus(final DdiActionFeedback feedback, final Target target, final Long actionid, final EntityFactory entityFactory) { @@ -561,11 +565,23 @@ public class DdiRootController implements DdiRootControllerRestApi { return status; } + private Target findTargetWithExceptionIfNotFound(final String controllerId) { + return controllerManagement.getByControllerId(controllerId) + .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); + } + private Action findActionWithExceptionIfNotFound(final Long actionId) { return controllerManagement.findActionWithDetails(actionId) .orElseThrow(() -> new EntityNotFoundException(Action.class, actionId)); } + private void verifyActionAssignedToTarget(final Target target, final Action action) { + if (!action.getTarget().getId().equals(target.getId())) { + LOG.warn(GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET, action.getId(), target.getId()); + throw new EntityNotFoundException(Action.class, action.getId()); + } + } + /** * If the action has a maintenance schedule defined but is no longer valid, * cancel the action. diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java index 892f8292f..efbc251d7 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java @@ -8,17 +8,32 @@ */ package org.eclipse.hawkbit.ddi.rest.resource; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.contains; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringWriter; import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.test.TestConfiguration; import org.eclipse.hawkbit.rest.AbstractRestIntegrationTest; import org.eclipse.hawkbit.rest.RestConfiguration; +import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; import org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration; +import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; @@ -32,6 +47,19 @@ import com.fasterxml.jackson.dataformat.cbor.CBORParser; @TestPropertySource(locations = "classpath:/ddi-test.properties") public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrationTest { + protected static final String HTTP_LOCALHOST = "http://localhost:8080/"; + protected static final String CONTROLLER_BASE = "/{tenant}/controller/v1/{controllerId}"; + + protected static final String SOFTWARE_MODULE_ARTIFACTS = CONTROLLER_BASE + + "/softwaremodules/{softwareModuleId}/artifacts"; + protected static final String DEPLOYMENT_BASE = CONTROLLER_BASE + "/deploymentBase/{actionId}"; + protected static final String CANCEL_ACTION = CONTROLLER_BASE + "/cancelAction/{actionId}"; + protected static final String INSTALLED_BASE = CONTROLLER_BASE + "/installedBase/{actionId}"; + protected static final String DEPLOYMENT_FEEDBACK = DEPLOYMENT_BASE + "/feedback"; + protected static final String CANCEL_FEEDBACK = CANCEL_ACTION + "/feedback"; + + protected static final int ARTIFACT_SIZE = 5 * 1024; + /** * Convert JSON to a CBOR equivalent. * @@ -75,4 +103,121 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat jsonGenerator.flush(); return stringWriter.toString(); } + + protected ResultActions postDeploymentFeedback(final String controllerId, final Long actionId, final String content, + final ResultMatcher statusMatcher) throws Exception { + return postDeploymentFeedback(MediaType.APPLICATION_JSON, controllerId, actionId, content.getBytes(), + statusMatcher); + } + + protected ResultActions postDeploymentFeedback(final MediaType mediaType, final String controllerId, + final Long actionId, final byte[] content, final ResultMatcher statusMatcher) throws Exception { + return mvc + .perform(post(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), controllerId, actionId) + .content(content).contentType(mediaType).accept(mediaType)) + .andDo(MockMvcResultPrinter.print()).andExpect(statusMatcher); + } + + protected ResultActions performGet(final String url, final MediaType mediaType, final ResultMatcher statusMatcher, + final String... values) throws Exception { + return mvc.perform(MockMvcRequestBuilders.get(url, values).accept(mediaType)) + .andDo(MockMvcResultPrinter.print()).andExpect(statusMatcher) + .andExpect(content().contentTypeCompatibleWith(mediaType)); + } + + protected ResultActions getAndVerifyDeploymentBasePayload(final String controllerId, final MediaType mediaType, + final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId, + final Long osModuleId, final String downloadType, final String updateType) throws Exception { + final ResultActions resultActions = performGet(DEPLOYMENT_BASE, mediaType, status().isOk(), + tenantAware.getCurrentTenant(), controllerId, actionId.toString()); + return verifyBasePayload(resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId, + downloadType, updateType); + } + + protected ResultActions getAndVerifyDeploymentBasePayload(final String controllerId, final MediaType mediaType, + final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId, + final Long osModuleId, final Action.ActionType actionType) throws Exception { + return getAndVerifyDeploymentBasePayload(controllerId, mediaType, ds, artifact, artifactSignature, actionId, + osModuleId, getDownloadAndUploadType(actionType), getDownloadAndUploadType(actionType)); + } + + protected ResultActions getAndVerifyInstalledBasePayload(final String controllerId, final MediaType mediaType, + final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId, + final Long osModuleId, final Action.ActionType actionType) throws Exception { + final ResultActions resultActions = performGet(INSTALLED_BASE, mediaType, status().isOk(), + tenantAware.getCurrentTenant(), controllerId, actionId.toString()); + return verifyBasePayload(resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId, + getDownloadAndUploadType(actionType), getDownloadAndUploadType(actionType)); + } + + private ResultActions verifyBasePayload(ResultActions resultActions, final String controllerId, + final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId, + final Long osModuleId, final String downloadType, final String updateType) throws Exception { + return resultActions.andExpect(jsonPath("$.id", equalTo(String.valueOf(actionId)))) + .andExpect(jsonPath("$.deployment.download", equalTo(downloadType))) + .andExpect(jsonPath("$.deployment.update", equalTo(updateType))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='jvm')].name", + contains(ds.findFirstModuleByType(runtimeType).get().getName()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='jvm')].version", + contains(ds.findFirstModuleByType(runtimeType).get().getVersion()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].name", + contains(ds.findFirstModuleByType(osType).get().getName()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].version", + contains(ds.findFirstModuleByType(osType).get().getVersion()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].size", contains(ARTIFACT_SIZE))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].filename", + contains(artifact.getFilename()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].hashes.md5", + contains(artifact.getMd5Hash()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].hashes.sha1", + contains(artifact.getSha1Hash()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].hashes.sha256", + contains(artifact.getSha256Hash()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0]._links.download-http.href", + contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId + + "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0]._links.md5sum-http.href", + contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId + + "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename() + + ".MD5SUM"))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].size", contains(ARTIFACT_SIZE))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].filename", + contains(artifactSignature.getFilename()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].hashes.md5", + contains(artifactSignature.getMd5Hash()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].hashes.sha1", + contains(artifactSignature.getSha1Hash()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].hashes.sha256", + contains(artifactSignature.getSha256Hash()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1]._links.download-http.href", + contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId + + "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1]._links.md5sum-http.href", + contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId + + "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename() + + ".MD5SUM"))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].version", + contains(ds.findFirstModuleByType(appType).get().getVersion()))) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].metadata").doesNotExist()) + .andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].name") + .value(ds.findFirstModuleByType(appType).get().getName())); + } + + protected String installedBaseLink(final String controllerId, final String actionId) { + return "http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId + + "/installedBase/" + actionId; + } + + protected String deploymentBaseLink(final String controllerId, final String actionId) { + return "http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId + + "/deploymentBase/" + actionId; + } + + private static String getDownloadAndUploadType(final Action.ActionType actionType) { + if (Action.ActionType.FORCED.equals(actionType)) { + return "forced"; + } + return "attempt"; + } + } diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java index dbd4fb945..d2808e5d5 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java @@ -10,13 +10,11 @@ package org.eclipse.hawkbit.ddi.rest.resource; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.startsWith; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -55,8 +53,6 @@ import org.springframework.data.domain.Sort.Direction; import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import com.jayway.jsonpath.JsonPath; @@ -72,12 +68,6 @@ import io.qameta.allure.Story; @Story("Deployment Action Resource") public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { - private static final String HTTP_LOCALHOST = "http://localhost:8080/"; - private static final String CONTROLLER_BY_ID = "/{tenant}/controller/v1/{controllerId}"; - private static final String SOFTWARE_MODULE_ARTIFACTS = CONTROLLER_BY_ID - + "/softwaremodules/{softwareModuleId}/artifacts"; - private static final String DEPLOYMENT_BASE = CONTROLLER_BY_ID + "/deploymentBase/"; - private static final int ARTIFACT_SIZE = 5 * 1024; private static final String DEFAULT_CONTROLLER_ID = "4712"; @Test @@ -91,9 +81,9 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { .getContent().get(0); // get deployment base - performGet(DEPLOYMENT_BASE + action.getId().toString(), + performGet(DEPLOYMENT_BASE, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR), status().isOk(), - tenantAware.getCurrentTenant(), target.getControllerId()); + tenantAware.getCurrentTenant(), target.getControllerId(), action.getId().toString()); final Long softwareModuleId = distributionSet.getModules().stream().findAny().get().getId(); testdataFactory.createArtifacts(softwareModuleId); @@ -105,7 +95,7 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { final byte[] feedback = jsonToCbor( JsonBuilder.deploymentActionFeedback(action.getId().toString(), "proceeding")); - postFeedback(MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR), target.getControllerId(), + postDeploymentFeedback(MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR), target.getControllerId(), action.getId(), feedback, status().isOk()); } @@ -173,12 +163,11 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { // Run test final long current = System.currentTimeMillis(); - performGet(CONTROLLER_BY_ID, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), + performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))) .andExpect(jsonPath("$._links.deploymentBase.href", - startsWith("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/" - + DEFAULT_CONTROLLER_ID + "/deploymentBase/" + uaction.getId()))); + startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, uaction.getId().toString())))); assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery()) .isGreaterThanOrEqualTo(current); assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery()) @@ -209,24 +198,23 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds.getId(), target.getControllerId(), ActionType.TIMEFORCED, System.currentTimeMillis() + 2_000)); - MvcResult mvcResult = performGet("/{tenant}/controller/v1/" + DEFAULT_CONTROLLER_ID, MediaTypes.HAL_JSON, - status().isOk(), tenantAware.getCurrentTenant()).andReturn(); + MvcResult mvcResult = performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), + tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID).andReturn(); final String urlBeforeSwitch = JsonPath.compile("_links.deploymentBase.href") .read(mvcResult.getResponse().getContentAsString()).toString(); // Time is not yet over, so we should see the same URL - mvcResult = performGet(CONTROLLER_BY_ID, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), + mvcResult = performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID).andReturn(); assertThat(JsonPath.compile("_links.deploymentBase.href").read(mvcResult.getResponse().getContentAsString()) .toString()).isEqualTo(urlBeforeSwitch) - .startsWith("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/" - + DEFAULT_CONTROLLER_ID + "/deploymentBase/" + actionId); + .startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, actionId.toString())); // After the time is over we should see a new etag TimeUnit.MILLISECONDS.sleep(2_000); - mvcResult = performGet(CONTROLLER_BY_ID, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), + mvcResult = performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID).andReturn(); assertThat(JsonPath.compile("_links.deploymentBase.href").read(mvcResult.getResponse().getContentAsString()) @@ -273,12 +261,11 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { // Run test final long current = System.currentTimeMillis(); - performGet(CONTROLLER_BY_ID, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), + performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))) .andExpect(jsonPath("$._links.deploymentBase.href", - startsWith("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/" - + DEFAULT_CONTROLLER_ID + "/deploymentBase/" + uaction.getId()))); + startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, uaction.getId().toString())))); assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery()) .isGreaterThanOrEqualTo(current); assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery()) @@ -333,12 +320,11 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { // Run test final long current = System.currentTimeMillis(); - performGet(CONTROLLER_BY_ID, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), + performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))) .andExpect(jsonPath("$._links.deploymentBase.href", - startsWith("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/" - + DEFAULT_CONTROLLER_ID + "/deploymentBase/" + uaction.getId()))); + startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, uaction.getId().toString())))); assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery()) .isGreaterThanOrEqualTo(current); assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery()) @@ -401,12 +387,11 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { // Run test final long current = System.currentTimeMillis(); - performGet(CONTROLLER_BY_ID, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), + performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))) .andExpect(jsonPath("$._links.deploymentBase.href", - startsWith("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/" - + DEFAULT_CONTROLLER_ID + "/deploymentBase/" + uaction.getId()))); + startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, uaction.getId().toString())))); assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery()) .isGreaterThanOrEqualTo(current); assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery()) @@ -439,97 +424,39 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { .value(visibleMetadataOsValue)); } - private ResultActions getAndVerifyDeploymentBasePayload(final String controllerId, final MediaType mediaType, - final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId, - final Long osModuleId, final String downloadType, final String updateType) throws Exception { - return performGet("/{tenant}/controller/v1/" + controllerId + "/deploymentBase/{actionId}", mediaType, - status().isOk(), tenantAware.getCurrentTenant(), actionId.toString()) - .andExpect(jsonPath("$.id", equalTo(String.valueOf(actionId)))) - .andExpect(jsonPath("$.deployment.download", equalTo(downloadType))) - .andExpect(jsonPath("$.deployment.update", equalTo(updateType))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='jvm')].name", - contains(ds.findFirstModuleByType(runtimeType).get().getName()))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='jvm')].version", - contains(ds.findFirstModuleByType(runtimeType).get().getVersion()))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].name", - contains(ds.findFirstModuleByType(osType).get().getName()))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].version", - contains(ds.findFirstModuleByType(osType).get().getVersion()))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].size", - contains(ARTIFACT_SIZE))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].filename", - contains("test1"))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].hashes.md5", - contains(artifact.getMd5Hash()))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].hashes.sha1", - contains(artifact.getSha1Hash()))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].hashes.sha256", - contains(artifact.getSha256Hash()))) - .andExpect(jsonPath( - "$.deployment.chunks[?(@.part=='os')].artifacts[0]._links.download-http.href", - contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" - + controllerId + "/softwaremodules/" + osModuleId + "/artifacts/test1"))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0]._links.md5sum-http.href", - contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" - + controllerId + "/softwaremodules/" + osModuleId + "/artifacts/test1.MD5SUM"))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].size", - contains(ARTIFACT_SIZE))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].filename", - contains("test1.signature"))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].hashes.md5", - contains(artifactSignature.getMd5Hash()))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].hashes.sha1", - contains(artifactSignature.getSha1Hash()))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].hashes.sha256", - contains(artifactSignature.getSha256Hash()))) - .andExpect( - jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1]._links.download-http.href", - contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" - + controllerId + "/softwaremodules/" + osModuleId - + "/artifacts/test1.signature"))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1]._links.md5sum-http.href", - contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" - + controllerId + "/softwaremodules/" + osModuleId - + "/artifacts/test1.signature.MD5SUM"))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].version", - contains(ds.findFirstModuleByType(appType).get().getVersion()))) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].metadata").doesNotExist()) - .andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].name") - .value(ds.findFirstModuleByType(appType).get().getName())); - } - @Test @Description("Test various invalid access attempts to the deployment resource und the expected behaviour of the server.") public void badDeploymentAction() throws Exception { final Target target = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID); // not allowed methods - mvc.perform(post(DEPLOYMENT_BASE + "1", tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID)) + mvc.perform(post(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "1")) .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); - mvc.perform(put(DEPLOYMENT_BASE + "1", tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID)) + mvc.perform(put(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "1")) .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); - mvc.perform(delete(DEPLOYMENT_BASE + "1", tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID)) + mvc.perform(delete(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "1")) .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); // non existing target - mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_BASE + "1", tenantAware.getCurrentTenant(), "not-existing")) + mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), "not-existing", "1")) .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); // no deployment - mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_BASE + "1", tenantAware.getCurrentTenant(), - DEFAULT_CONTROLLER_ID)).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + mvc.perform( + MockMvcRequestBuilders.get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "1")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); // wrong media type final List toAssign = Collections.singletonList(target); final DistributionSet savedSet = testdataFactory.createDistributionSet(""); final Long actionId = getFirstAssignedActionId(assignDistributionSet(savedSet, toAssign)); - mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_BASE + actionId, tenantAware.getCurrentTenant(), - DEFAULT_CONTROLLER_ID)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, + actionId)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); mvc.perform(MockMvcRequestBuilders - .get(DEPLOYMENT_BASE + actionId, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID) + .get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, actionId) .accept(MediaType.APPLICATION_ATOM_XML)).andDo(MockMvcResultPrinter.print()) .andExpect(status().isNotAcceptable()); } @@ -548,11 +475,10 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { final String feedback = JsonBuilder.deploymentActionFeedback(action.getId().toString(), "proceeding"); // assign distribution set creates an action status, so only 99 left for (int i = 0; i < 99; i++) { - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isOk()); + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isOk()); } - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, action.getId(), feedback, - status().isForbidden()); + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isForbidden()); } @Test @@ -573,7 +499,7 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { final String feedback = JsonBuilder.deploymentActionFeedback(action.getId().toString(), "proceeding", "none", messages); - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, action.getId(), feedback, + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isForbidden()); } @@ -596,21 +522,21 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { assertThat(targetManagement.findByUpdateStatus(PageRequest.of(0, 10), TargetUpdateStatus.UNKNOWN)).hasSize(2); // action1 done - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, actionId1, + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId1, JsonBuilder.deploymentActionFeedback(actionId1.toString(), "closed"), status().isOk()); findTargetAndAssertUpdateStatus(Optional.of(ds3), TargetUpdateStatus.PENDING, 2, Optional.of(ds1)); assertStatusMessagesCount(4); // action2 done - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, actionId2, + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId2, JsonBuilder.deploymentActionFeedback(actionId2.toString(), "closed"), status().isOk()); findTargetAndAssertUpdateStatus(Optional.of(ds3), TargetUpdateStatus.PENDING, 1, Optional.of(ds2)); assertStatusMessagesCount(5); // action3 done - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, actionId3, + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId3, JsonBuilder.deploymentActionFeedback(actionId3.toString(), "closed"), status().isOk()); findTargetAndAssertUpdateStatus(Optional.of(ds3), TargetUpdateStatus.IN_SYNC, 0, Optional.of(ds3)); @@ -629,7 +555,7 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { assignDistributionSet(ds, Collections.singletonList(savedTarget)); final Action action = deploymentManagement.findActionsByDistributionSet(PAGE, ds.getId()).getContent().get(0); - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, action.getId(), + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), JsonBuilder.deploymentActionFeedback(action.getId().toString(), "closed", "failure", "error message"), status().isOk()); @@ -643,7 +569,7 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { Collections.singletonList(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get())); final Action action2 = deploymentManagement.findActiveActionsByTarget(PAGE, DEFAULT_CONTROLLER_ID).getContent() .get(0); - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, action2.getId(), + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action2.getId(), JsonBuilder.deploymentActionFeedback(action2.getId().toString(), "closed", "SUCCESS"), status().isOk()); findTargetAndAssertUpdateStatus(Optional.of(ds), TargetUpdateStatus.IN_SYNC, 0, Optional.of(ds)); assertTargetCountByStatus(0, 0, 1); @@ -666,32 +592,32 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { // Now valid Feedback for (int i = 0; i < 4; i++) { - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, actionId, + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, JsonBuilder.deploymentActionFeedback(actionId.toString(), "proceeding"), status().isOk()); assertActionStatusCount(i + 2, i); } - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, actionId, + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, JsonBuilder.deploymentActionFeedback(actionId.toString(), "scheduled"), status().isOk()); assertActionStatusCount(6, 5); - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, actionId, + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, JsonBuilder.deploymentActionFeedback(actionId.toString(), "resumed"), status().isOk()); assertActionStatusCount(7, 6); - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, actionId, + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, JsonBuilder.deploymentActionFeedback(actionId.toString(), "canceled"), status().isOk()); assertStatusAndActiveActionsCount(TargetUpdateStatus.PENDING, 1); assertActionStatusCount(8, 7, 0, 0, 1); assertTargetCountByStatus(1, 0, 0); - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, actionId, + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, JsonBuilder.deploymentActionFeedback(actionId.toString(), "rejected"), status().isOk()); assertStatusAndActiveActionsCount(TargetUpdateStatus.PENDING, 1); assertActionStatusCount(9, 6, 1, 0, 1); - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, actionId, + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, JsonBuilder.deploymentActionFeedback(actionId.toString(), "closed"), status().isOk()); assertStatusAndActiveActionsCount(TargetUpdateStatus.IN_SYNC, 0); assertActionStatusCount(10, 7, 1, 1, 1); @@ -709,13 +635,13 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { // target does not exist - postFeedback(MediaType.APPLICATION_JSON, DEFAULT_CONTROLLER_ID, 1234L, + postDeploymentFeedback(DEFAULT_CONTROLLER_ID, 1234L, JsonBuilder.deploymentActionInProgressFeedback("1234"), status().isNotFound()); final Target savedTarget = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID); - // Action does not exists - postFeedback(MediaType.APPLICATION_JSON, "4713", 1234L, JsonBuilder.deploymentActionInProgressFeedback("1234"), + // Action does not exist + postDeploymentFeedback("4713", 1234L, JsonBuilder.deploymentActionInProgressFeedback("1234"), status().isNotFound()); assignDistributionSet(savedSet, Collections.singletonList(savedTarget)).getAssignedEntity().iterator().next(); @@ -725,17 +651,18 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { .getContent().get(0); // action exists but is not assigned to this target - postFeedback(MediaType.APPLICATION_JSON, "4713", updateAction.getId(), + postDeploymentFeedback("4713", updateAction.getId(), JsonBuilder.deploymentActionInProgressFeedback(updateAction.getId().toString()), status().isNotFound()); // not allowed methods - mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_BASE + "2/feedback", tenantAware.getCurrentTenant(), - DEFAULT_CONTROLLER_ID)).andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), + DEFAULT_CONTROLLER_ID, "2")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); - mvc.perform(put(DEPLOYMENT_BASE + "2/feedback", tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID)) + mvc.perform(put(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "2")) .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); - mvc.perform(delete(DEPLOYMENT_BASE + "2/feedback", tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID)) + mvc.perform(delete(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "2")) .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); } @@ -754,7 +681,7 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { assignDistributionSet(ds.getId(), "1080"); final Action action = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent() .get(0); - postFeedback(MediaType.APPLICATION_JSON, "1080", action.getId(), + postDeploymentFeedback("1080", action.getId(), JsonBuilder.deploymentActionInProgressFeedback("AAAA"), status().isBadRequest()); } @@ -775,7 +702,7 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { .get(0); final String missingResultInFeedback = JsonBuilder.missingResultInFeedback(action.getId().toString(), "closed", "test"); - postFeedback(MediaType.APPLICATION_JSON, "1080", action.getId(), missingResultInFeedback, + postDeploymentFeedback("1080", action.getId(), missingResultInFeedback, status().isBadRequest()).andExpect(jsonPath("$.*", hasSize(3))).andExpect( jsonPath("$.exceptionClass", equalTo(MessageNotReadableException.class.getCanonicalName()))); } @@ -797,7 +724,7 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { .get(0); final String missingFinishedResultInFeedback = JsonBuilder .missingFinishedResultInFeedback(action.getId().toString(), "closed", "test"); - postFeedback(MediaType.APPLICATION_JSON, "1080", action.getId(), missingFinishedResultInFeedback, + postDeploymentFeedback("1080", action.getId(), missingFinishedResultInFeedback, status().isBadRequest()).andExpect(jsonPath("$.*", hasSize(3))).andExpect( jsonPath("$.exceptionClass", equalTo(MessageNotReadableException.class.getCanonicalName()))); } @@ -814,7 +741,7 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { new ActionStatusCondition(Status.RUNNING)); } - private class ActionStatusCondition extends Condition { + private static class ActionStatusCondition extends Condition { private final Status status; public ActionStatusCondition(final Status status) { @@ -827,31 +754,11 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { } } - private ResultActions performGet(final String url, final MediaType mediaType, final ResultMatcher statusMatcher, - final String... values) throws Exception { - return mvc.perform(MockMvcRequestBuilders.get(url, values).accept(mediaType)) - .andDo(MockMvcResultPrinter.print()).andExpect(statusMatcher) - .andExpect(content().contentTypeCompatibleWith(mediaType)); - } - - private ResultActions postFeedback(final MediaType mediaType, final String controllerId, final Long id, - final String content, final ResultMatcher statusMatcher) throws Exception { - return postFeedback(mediaType, controllerId, id, content.getBytes(), statusMatcher); - } - - private ResultActions postFeedback(final MediaType mediaType, final String controllerId, final Long id, - final byte[] content, final ResultMatcher statusMatcher) throws Exception { - return mvc - .perform(post(DEPLOYMENT_BASE + id + "/feedback", tenantAware.getCurrentTenant(), controllerId) - .content(content).contentType(mediaType).accept(mediaType)) - .andDo(MockMvcResultPrinter.print()).andExpect(statusMatcher); - } - private Target createTargetAndAssertNoActiveActions() { final Target savedTarget = testdataFactory.createTarget(DdiDeploymentBaseTest.DEFAULT_CONTROLLER_ID); assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty(); - assertThat(deploymentManagement.countActionsAll()).isEqualTo(0); - assertThat(deploymentManagement.countActionStatusAll()).isEqualTo(0); + assertThat(deploymentManagement.countActionsAll()).isZero(); + assertThat(deploymentManagement.countActionStatusAll()).isZero(); return savedTarget; } diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiInstalledBaseTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiInstalledBaseTest.java new file mode 100644 index 000000000..81c0c7355 --- /dev/null +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiInstalledBaseTest.java @@ -0,0 +1,415 @@ +/** + * Copyright (c) 2022 Bosch.IO 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.ddi.rest.resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import org.apache.commons.lang3.RandomUtils; +import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants; +import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; +import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; +import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.test.matcher.Expect; +import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; +import org.eclipse.hawkbit.rest.util.JsonBuilder; +import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.hateoas.MediaTypes; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; + +/** + * Test installed base from the controller. + */ +@Feature("Component Tests - Direct Device Integration API") +@Story("Installed Base Resource") +public class DdiInstalledBaseTest extends AbstractDDiApiIntegrationTest { + + private static final int ARTIFACT_SIZE = 5 * 1024; + private static final String CONTROLLER_ID = "4715"; + + @Test + @Description("Ensure that the installed base resource is available as CBOR") + public void installedBaseResourceCbor() throws Exception { + final Target target = testdataFactory.createTarget(); + final DistributionSet ds = testdataFactory.createDistributionSet(""); + final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds, target)); + postDeploymentFeedback(target.getControllerId(), actionId, + JsonBuilder.deploymentActionFeedback(actionId.toString(), "closed"), status().isOk()); + + // get installed base + performGet(INSTALLED_BASE, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR), status().isOk(), + tenantAware.getCurrentTenant(), target.getControllerId(), actionId.toString()); + + final Long softwareModuleId = ds.getModules().stream().findAny().get().getId(); + testdataFactory.createArtifacts(softwareModuleId); + // get artifacts + performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR), + status().isOk(), tenantAware.getCurrentTenant(), target.getControllerId(), + String.valueOf(softwareModuleId)); + } + + @Test + @Description("Test several deployments to a controller. Checks that action is represented as installedBase after installation.") + public void deploymentSeveralActionsInInstalledBase() throws Exception { + // Prepare test data + final Target target = createTargetAndAssertNoActiveActions(); + + final DistributionSet ds1 = testdataFactory.createDistributionSet("1", true); + final Artifact artifact1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), + getOsModule(ds1), "test1", ARTIFACT_SIZE); + final Artifact artifactSignature1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), + getOsModule(ds1), "test1.signature", ARTIFACT_SIZE); + + final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true); + final Artifact artifact2 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), + getOsModule(ds2), "test2", ARTIFACT_SIZE); + final Artifact artifactSignature2 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), + getOsModule(ds2), "test2.signature", ARTIFACT_SIZE); + + // Run test with 1st action + final Long actionId1 = getFirstAssignedActionId( + assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT)); + performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID) + .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$._links.installedBase.href").doesNotExist()) + .andExpect(jsonPath("$._links.deploymentBase.href", + startsWith(deploymentBaseLink(CONTROLLER_ID, actionId1.toString())))); + + getAndVerifyDeploymentBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1, + actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT); + + postDeploymentFeedback(target.getControllerId(), actionId1, + JsonBuilder.deploymentActionFeedback(actionId1.toString(), "closed", "success", "Closed"), + status().isOk()); + + getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1, + actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT); + + // Run test with 2nd action + final Long actionId2 = getFirstAssignedActionId( + assignDistributionSet(ds2.getId(), target.getControllerId(), Action.ActionType.FORCED)); + performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID) + .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$._links.installedBase.href", + startsWith(installedBaseLink(CONTROLLER_ID, actionId1.toString())))) + .andExpect(jsonPath("$._links.deploymentBase.href", + startsWith(deploymentBaseLink(CONTROLLER_ID, actionId2.toString())))); + + getAndVerifyDeploymentBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds2, artifact2, artifactSignature2, + actionId2, ds2.findFirstModuleByType(osType).get().getId(), Action.ActionType.FORCED); + + postDeploymentFeedback(target.getControllerId(), actionId2, + JsonBuilder.deploymentActionFeedback(actionId2.toString(), "closed", "success", "Closed"), + status().isOk()); + + getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds2, artifact2, artifactSignature2, + actionId2, ds2.findFirstModuleByType(osType).get().getId(), Action.ActionType.FORCED); + + performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID) + .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$._links.installedBase.href", + startsWith(installedBaseLink(CONTROLLER_ID, actionId2.toString())))) + .andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist()); + + // older installed action is still accessible, although not part of controller base + getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1, + actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT); + } + + @Test + @Description("Test open deployment to a controller. Checks that installedBase returns 404 for a pending action.") + public void installedBaseReturns404ForPendingAction() throws Exception { + // Prepare test data + final Target target = createTargetAndAssertNoActiveActions(); + final DistributionSet ds = testdataFactory.createDistributionSet(""); + final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds, target)); + + performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID) + .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$._links.installedBase.href").doesNotExist()) + .andExpect(jsonPath("$._links.deploymentBase.href", + startsWith(deploymentBaseLink(CONTROLLER_ID, actionId.toString())))); + + performGet(DEPLOYMENT_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(), + target.getControllerId(), actionId.toString()); + + mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(), + actionId.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + } + + @Test + @Description("Ensures that artifacts are found, after the action was already closed.") + public void artifactsOfInstalledActionExist() throws Exception { + final Target target = createTargetAndAssertNoActiveActions(); + final DistributionSet ds = testdataFactory.createDistributionSet(""); + final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds, target)); + + postDeploymentFeedback(target.getControllerId(), actionId, + JsonBuilder.deploymentActionFeedback(actionId.toString(), "closed"), status().isOk()); + + final Long softwareModuleId = ds.getModules().stream().findAny().get().getId(); + performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.APPLICATION_JSON, status().isOk(), + tenantAware.getCurrentTenant(), target.getControllerId(), softwareModuleId.toString()) + .andExpect(jsonPath("$", hasSize(0))); + + testdataFactory.createArtifacts(softwareModuleId); + + performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.APPLICATION_JSON, status().isOk(), + tenantAware.getCurrentTenant(), target.getControllerId(), softwareModuleId.toString()) + .andExpect(jsonPath("$", hasSize(3))) + .andExpect(jsonPath("$.[?(@.filename=='filename0')]", hasSize(1))) + .andExpect(jsonPath("$.[?(@.filename=='filename1')]", hasSize(1))) + .andExpect(jsonPath("$.[?(@.filename=='filename2')]", hasSize(1))); + + } + + private static Stream actionTypeForDeployment() { + return Stream.of(Action.ActionType.SOFT, Action.ActionType.FORCED); + } + + @ParameterizedTest + @MethodSource("org.eclipse.hawkbit.ddi.rest.resource.DdiInstalledBaseTest#actionTypeForDeployment") + @Description("Test forced deployment to a controller. Checks that action is represented as installedBase after installation.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = TargetAttributesRequestedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 2), + @Expect(type = TargetPollEvent.class, count = 1) }) + public void deploymentActionInInstalledBase(final Action.ActionType actionType) throws Exception { + // Prepare test data + final Target target = createTargetAndAssertNoActiveActions(); + final DistributionSet ds = testdataFactory.createDistributionSet("", true); + final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds), + "test1", ARTIFACT_SIZE); + final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), + getOsModule(ds), "test1.signature", ARTIFACT_SIZE); + final Long actionId = getFirstAssignedActionId( + assignDistributionSet(ds.getId(), target.getControllerId(), actionType)); + + postDeploymentFeedback(target.getControllerId(), actionId, + JsonBuilder.deploymentActionFeedback(actionId.toString(), "closed", "success", "Closed"), + status().isOk()); + + // Run test + final ResultActions resultActions = performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), + tenantAware.getCurrentTenant(), target.getControllerId()); + resultActions.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist()) + .andExpect(jsonPath("$._links.installedBase.href", + containsString(String.format("/%s/controller/v1/%s/installedBase/%d", + tenantAware.getCurrentTenant(), target.getControllerId(), actionId)))); + + getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, artifact, artifactSignature, + actionId, ds.findFirstModuleByType(osType).get().getId(), actionType); + + getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaTypes.HAL_JSON, ds, artifact, artifactSignature, actionId, + ds.findFirstModuleByType(osType).get().getId(), actionType); + + // Action is still finished after calling installedBase + final Iterable actionStatusMessages = deploymentManagement + .findActionStatusByAction(PageRequest.of(0, 100, Sort.Direction.DESC, "id"), actionId); + assertThat(actionStatusMessages).hasSize(2); + final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next(); + assertThat(actionStatusMessage.getStatus()).isEqualTo(Action.Status.FINISHED); + } + + @Test + @Description("Test download-only deployment to a controller. Checks that download-only is not represented as installedBase.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = TargetAttributesRequestedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = TargetPollEvent.class, count = 2) }) + public void deploymentDownloadOnlyActionNotInInstalledBase() throws Exception { + // Prepare test data + final Target target = testdataFactory.createTarget(); + final DistributionSet ds = testdataFactory.createDistributionSet(""); + final Long actionId = getFirstAssignedActionId( + assignDistributionSet(ds.getId(), target.getControllerId(), Action.ActionType.DOWNLOAD_ONLY)); + + performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(), + target.getControllerId()).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$._links.deploymentBase.href").exists()) + .andExpect(jsonPath("$._links.installedBase.href").doesNotExist()); + + postDeploymentFeedback(target.getControllerId(), actionId, + JsonBuilder.deploymentActionFeedback(actionId.toString(), "download"), status().isOk()); + postDeploymentFeedback(target.getControllerId(), actionId, + JsonBuilder.deploymentActionFeedback(actionId.toString(), "downloaded"), status().isOk()); + + // Test + performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(), + target.getControllerId()).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist()) + .andExpect(jsonPath("$._links.installedBase.href").doesNotExist()); + } + + @ParameterizedTest + @MethodSource("org.eclipse.hawkbit.ddi.rest.resource.DdiInstalledBaseTest#actionTypeForDeployment") + @Description("Test a failed deployment to a controller. Checks that closed action is not represented as installedBase.") + public void deploymentActionFailedNotInInstalledBase(Action.ActionType actionType) throws Exception { + // Prepare test data + final Target target = testdataFactory.createTarget(); + final DistributionSet ds = testdataFactory.createDistributionSet(""); + final Long actionId = getFirstAssignedActionId( + assignDistributionSet(ds.getId(), target.getControllerId(), actionType)); + + performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(), + target.getControllerId()).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$._links.deploymentBase.href").exists()) + .andExpect(jsonPath("$._links.installedBase.href").doesNotExist()); + + postDeploymentFeedback(target.getControllerId(), actionId, + JsonBuilder.deploymentActionFeedback(actionId.toString(), "proceeding"), status().isOk()); + postDeploymentFeedback(target.getControllerId(), actionId, + JsonBuilder.deploymentActionFeedback(actionId.toString(), "closed", "failure", "Installation failed"), + status().isOk()); + + // Test + performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(), + target.getControllerId()).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist()) + .andExpect(jsonPath("$._links.installedBase.href").doesNotExist()); + } + + @Test + @Description("Test to verify that only a specific count of messages are returned based on the input actionHistory for getControllerInstalledAction endpoint.") + public void testActionHistoryCount() throws Exception { + final DistributionSet ds = testdataFactory.createDistributionSet(""); + Target savedTarget = testdataFactory.createTarget("911"); + savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId())); + final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) + .getContent().get(0); + + postDeploymentFeedback(savedTarget.getControllerId(), savedAction.getId(), JsonBuilder.deploymentActionFeedback( + savedAction.getId().toString(), "scheduled", "Installation scheduled"), status().isOk()); + + postDeploymentFeedback(savedTarget.getControllerId(), savedAction.getId(), JsonBuilder.deploymentActionFeedback( + savedAction.getId().toString(), "proceeding", "Installation proceeding"), status().isOk()); + // only this feedback triggers the ActionUpdateEvent + postDeploymentFeedback(savedTarget.getControllerId(), savedAction.getId(), JsonBuilder.deploymentActionFeedback( + savedAction.getId().toString(), "closed", "success", "Installation completed"), status().isOk()); + + // Test + // for zero input no action history is returned + mvc.perform(get(INSTALLED_BASE + "?actionHistory", tenantAware.getCurrentTenant(), 911, savedAction.getId()) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.actionHistory.messages").doesNotExist()); + + // depending on given query parameter value, only the latest messages are + // returned + mvc.perform(get(INSTALLED_BASE + "?actionHistory=2", tenantAware.getCurrentTenant(), 911, savedAction.getId()) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation completed")))) + .andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation proceeding")))) + .andExpect( + jsonPath("$.actionHistory.messages", not(hasItem(containsString("Installation scheduled"))))); + + // for negative input the entire action history is returned + mvc.perform(get(INSTALLED_BASE + "?actionHistory=-3", tenantAware.getCurrentTenant(), 911, savedAction.getId()) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation completed")))) + .andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation proceeding")))) + .andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation scheduled")))); + } + + @Test + @Description("Test various invalid access attempts to the installed resource und the expected behaviour of the server.") + public void badInstalledAction() throws Exception { + final Target target = testdataFactory.createTarget(CONTROLLER_ID); + + // not allowed methods + mvc.perform(post(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, "1")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + mvc.perform(put(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, "1")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, "1")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + // non existing target + mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), "not-existing", "1")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // no deployment + mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, "1")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // wrong media type + final List toAssign = Collections.singletonList(target); + final DistributionSet savedSet = testdataFactory.createDistributionSet(""); + + final Long actionId = getFirstAssignedActionId(assignDistributionSet(savedSet, toAssign)); + postDeploymentFeedback(CONTROLLER_ID, actionId, + JsonBuilder.deploymentActionFeedback(actionId.toString(), "closed", "success"), status().isOk()); + mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, actionId)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, actionId) + .accept(MediaType.APPLICATION_ATOM_XML)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotAcceptable()); + } + + private Target createTargetAndAssertNoActiveActions() { + final Target savedTarget = testdataFactory.createTarget(CONTROLLER_ID); + assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty(); + assertThat(deploymentManagement.countActionsAll()).isZero(); + assertThat(deploymentManagement.countActionStatusAll()).isZero(); + return savedTarget; + } + +} diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java index c86863bf9..d694be1ab 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java @@ -18,6 +18,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -87,7 +88,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Ensure that the root poll resource is available as CBOR") public void rootPollResourceCbor() throws Exception { - mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711) .accept(DdiRestConstants.MEDIA_TYPE_CBOR)).andDo(MockMvcResultPrinter.print()) .andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR)).andExpect(status().isOk()); } @@ -95,7 +96,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @Description("Ensures that the API returns JSON when no Accept header is specified by the client.") public void apiReturnsJSONByDefault() throws Exception { - final MvcResult result = mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())) + final MvcResult result = mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(content().contentType(MediaTypes.HAL_JSON)).andReturn(); @@ -120,13 +121,13 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { // create tenant -- creates softwaremoduletypes and distributionsettypes systemManagement.getTenantMetadata("tenantDoesNotExists"); - mvc.perform(get("/{}/controller/v1/aControllerId", tenantAware.getCurrentTenant())) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), "aControllerId")) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); // delete tenant again, will also deleted target aControllerId systemManagement.deleteTenant("tenantDoesNotExists"); - mvc.perform(get("/{}/controller/v1/aControllerId", tenantAware.getCurrentTenant())) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), "aControllerId")) .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); } @@ -148,7 +149,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { // make a poll, audit information should not be changed, run as // controller principal! WithSpringAuthorityRule.runAs(WithSpringAuthorityRule.withController("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { - mvc.perform(get("/{tenant}/controller/v1/" + knownTargetControllerId, tenantAware.getCurrentTenant())) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), knownTargetControllerId)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); return null; }); @@ -162,7 +163,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { } @Test - @Description("Ensures that server returns a not found response in case of empty controlloer ID.") + @Description("Ensures that server returns a not found response in case of empty controller ID.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) }) public void rootRsWithoutId() throws Exception { mvc.perform(get("/controller/v1/")).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); @@ -175,24 +176,25 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { public void rootRsPlugAndPlay() throws Exception { final long current = System.currentTimeMillis(); + String controllerId = "4711"; - mvc.perform(get("/default-tenant/controller/v1/4711")).andDo(MockMvcResultPrinter.print()) + mvc.perform(get(CONTROLLER_BASE, "default-tenant", controllerId)).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()).andExpect(content().contentType(MediaTypes.HAL_JSON)) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))); - assertThat(targetManagement.getByControllerID("4711").get().getLastTargetQuery()) + assertThat(targetManagement.getByControllerID(controllerId).get().getLastTargetQuery()) .isGreaterThanOrEqualTo(current); - assertThat(targetManagement.getByControllerID("4711").get().getUpdateStatus()) + assertThat(targetManagement.getByControllerID(controllerId).get().getUpdateStatus()) .isEqualTo(TargetUpdateStatus.REGISTERED); // not allowed methods - mvc.perform(post("/default-tenant/controller/v1/4711")).andDo(MockMvcResultPrinter.print()) + mvc.perform(post(CONTROLLER_BASE, "default-tenant", controllerId)).andDo(MockMvcResultPrinter.print()) .andExpect(status().isMethodNotAllowed()); - mvc.perform(put("/default-tenant/controller/v1/4711")).andDo(MockMvcResultPrinter.print()) + mvc.perform(put(CONTROLLER_BASE, "default-tenant", controllerId)).andDo(MockMvcResultPrinter.print()) .andExpect(status().isMethodNotAllowed()); - mvc.perform(delete("/default-tenant/controller/v1/4711")).andDo(MockMvcResultPrinter.print()) + mvc.perform(delete(CONTROLLER_BASE, "default-tenant", controllerId)).andDo(MockMvcResultPrinter.print()) .andExpect(status().isMethodNotAllowed()); } @@ -210,7 +212,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { }); WithSpringAuthorityRule.runAs(WithSpringAuthorityRule.withUser("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { - mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(content().contentType(MediaTypes.HAL_JSON)) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:02:00"))); @@ -229,63 +231,76 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Expect(type = TargetAttributesRequestedEvent.class, count = 1), @Expect(type = SoftwareModuleCreatedEvent.class, count = 6) }) public void rootRsNotModified() throws Exception { - final String etag = mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())) + String controllerId = "4711"; + final String etag = mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist()) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))).andReturn().getResponse() .getHeader("ETag"); - mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()).header("If-None-Match", etag)) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).header("If-None-Match", etag)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotModified()); - final Target target = targetManagement.getByControllerID("4711").get(); + final Target target = targetManagement.getByControllerID(controllerId).get(); final DistributionSet ds = testdataFactory.createDistributionSet(""); - assignDistributionSet(ds.getId(), "4711"); + assignDistributionSet(ds.getId(), controllerId); final Action updateAction = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId()) .getContent().get(0); final String etagWithFirstUpdate = mvc - .perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()) + .perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId) .header("If-None-Match", etag).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$._links.installedBase.href").doesNotExist()) .andExpect(jsonPath("$._links.deploymentBase.href", - startsWith("http://localhost/" + tenantAware.getCurrentTenant() - + "/controller/v1/4711/deploymentBase/" + updateAction.getId()))) + startsWith(deploymentBaseLink("4711", updateAction.getId().toString())))) .andReturn().getResponse().getHeader("ETag"); assertThat(etagWithFirstUpdate).isNotNull(); - mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()).header("If-None-Match", + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).header("If-None-Match", etagWithFirstUpdate)).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotModified()); // now lets finish the update sendDeploymentActionFeedback(target, updateAction, "closed", null).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - // we are again at the original state - mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()).header("If-None-Match", etag)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotModified()); + // as the update was installed, and we always receive the installed action, the + // original state cannot be restored + final String etagAfterInstallation = mvc + .perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId) + .header("If-None-Match", etag).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist()) + .andExpect(jsonPath("$._links.installedBase.href", + startsWith(installedBaseLink("4711", updateAction.getId().toString())))) + .andReturn().getResponse().getHeader("ETag"); // Now another deployment final DistributionSet ds2 = testdataFactory.createDistributionSet("2"); - assignDistributionSet(ds2.getId(), "4711"); + assignDistributionSet(ds2.getId(), controllerId); final Action updateAction2 = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId()) .getContent().get(0); - mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()) - .header("If-None-Match", etagWithFirstUpdate).accept(MediaType.APPLICATION_JSON)) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId) + .header("If-None-Match", etagAfterInstallation).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$._links.installedBase.href", + startsWith(installedBaseLink("4711", updateAction.getId().toString())))) .andExpect(jsonPath("$._links.deploymentBase.href", - startsWith("http://localhost/" + tenantAware.getCurrentTenant() - + "/controller/v1/4711/deploymentBase/" + updateAction2.getId()))) + startsWith(deploymentBaseLink("4711", updateAction2.getId().toString())))) .andReturn().getResponse().getHeader("ETag"); + } @Test @@ -294,23 +309,24 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) }) public void rootRsPrecommissioned() throws Exception { - testdataFactory.createTarget("4711"); + String controllerId = "4711"; + testdataFactory.createTarget(controllerId); - assertThat(targetManagement.getByControllerID("4711").get().getUpdateStatus()) + assertThat(targetManagement.getByControllerID(controllerId).get().getUpdateStatus()) .isEqualTo(TargetUpdateStatus.UNKNOWN); final long current = System.currentTimeMillis(); - mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(content().contentType(MediaTypes.HAL_JSON)) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))); - assertThat(targetManagement.getByControllerID("4711").get().getLastTargetQuery()) + assertThat(targetManagement.getByControllerID(controllerId).get().getLastTargetQuery()) .isLessThanOrEqualTo(System.currentTimeMillis()); - assertThat(targetManagement.getByControllerID("4711").get().getLastTargetQuery()) + assertThat(targetManagement.getByControllerID(controllerId).get().getLastTargetQuery()) .isGreaterThanOrEqualTo(current); - assertThat(targetManagement.getByControllerID("4711").get().getUpdateStatus()) + assertThat(targetManagement.getByControllerID(controllerId).get().getUpdateStatus()) .isEqualTo(TargetUpdateStatus.REGISTERED); } @@ -326,7 +342,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { // make a poll, audit information should be set on plug and play WithSpringAuthorityRule.runAs(WithSpringAuthorityRule.withController("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { mvc.perform( - get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(), knownControllerId1)) + get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), knownControllerId1)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); return null; }); @@ -350,7 +366,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { // test final String knownControllerId1 = "0815"; - mvc.perform(get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(), knownControllerId1)) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), knownControllerId1)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); // verify @@ -399,7 +415,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { final Map attributes = Collections.singletonMap("AttributeKey", "AttributeValue"); assertThatAttributesUpdateIsRequested(savedTarget.getControllerId()); - mvc.perform(put("/{tenant}/controller/v1/{controllerId}/configData", tenantAware.getCurrentTenant(), + mvc.perform(put(CONTROLLER_BASE + "/configData", tenantAware.getCurrentTenant(), savedTarget.getControllerId()).content(JsonBuilder.configData(attributes).toString()) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); @@ -431,13 +447,13 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { } private void assertThatAttributesUpdateIsRequested(final String targetControllerId) throws Exception { - mvc.perform(get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(), targetControllerId) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), targetControllerId) .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) .andExpect(jsonPath("$._links.configData.href").isNotEmpty()); } private void assertThatAttributesUpdateIsNotRequested(final String targetControllerId) throws Exception { - mvc.perform(get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(), targetControllerId) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), targetControllerId) .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) .andExpect(jsonPath("$._links.configData").doesNotExist()); } @@ -452,9 +468,9 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { } final String feedback = JsonBuilder.deploymentActionFeedback(action.getId().toString(), execution, finished, message); - return mvc.perform(post("/{tenant}/controller/v1/{controllerId}/deploymentBase/{actionId}/feedback", - tenantAware.getCurrentTenant(), target.getControllerId(), action.getId()).content(feedback) - .contentType(MediaType.APPLICATION_JSON)); + return mvc.perform( + post(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), target.getControllerId(), action.getId()) + .content(feedback).contentType(MediaType.APPLICATION_JSON)); } private ResultActions sendDeploymentActionFeedback(final Target target, final Action action, final String execution, @@ -487,14 +503,16 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success", TARGET_COMPLETED_INSTALLATION_MSG) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); - mvc.perform(get("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "?actionHistory=3", - tenantAware.getCurrentTenant()).contentType(MediaType.APPLICATION_JSON) + mvc.perform(get(DEPLOYMENT_BASE + "?actionHistory=2", tenantAware.getCurrentTenant(), 911, savedAction.getId()) + .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.actionHistory.messages", + hasItem(containsString(TARGET_COMPLETED_INSTALLATION_MSG)))) .andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString(TARGET_PROCEEDING_INSTALLATION_MSG)))) .andExpect(jsonPath("$.actionHistory.messages", - hasItem(containsString(TARGET_SCHEDULED_INSTALLATION_MSG)))); + not(hasItem(containsString(TARGET_SCHEDULED_INSTALLATION_MSG))))); } @Test @@ -522,10 +540,16 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success", TARGET_COMPLETED_INSTALLATION_MSG) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); - mvc.perform(get("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "?actionHistory=-2", - tenantAware.getCurrentTenant()).contentType(MediaType.APPLICATION_JSON) + mvc.perform(get(DEPLOYMENT_BASE + "?actionHistory=0", tenantAware.getCurrentTenant(), 911, savedAction.getId()) + .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.actionHistory.messages").doesNotExist()); + + mvc.perform(get(DEPLOYMENT_BASE + "?actionHistory", tenantAware.getCurrentTenant(), 911, savedAction.getId()) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.actionHistory.messages").doesNotExist()); } @Test @@ -553,8 +577,8 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success", TARGET_COMPLETED_INSTALLATION_MSG) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); - mvc.perform(get("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "?actionHistory=-1", - tenantAware.getCurrentTenant()).contentType(MediaType.APPLICATION_JSON) + mvc.perform(get(DEPLOYMENT_BASE + "?actionHistory=-1", tenantAware.getCurrentTenant(), 911, savedAction.getId()) + .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(jsonPath("$.actionHistory.messages", @@ -582,7 +606,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { assignDistributionSetWithMaintenanceWindow(ds.getId(), savedTarget.getControllerId(), getTestSchedule(16), getTestDuration(10), getTestTimeZone()).getAssignedEntity().iterator().next(); - mvc.perform(get("/default-tenant/controller/v1/1911/")).andExpect(status().isOk()) + mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")).andExpect(status().isOk()) .andExpect(jsonPath("$.config.polling.sleep", greaterThanOrEqualTo("00:05:00"))); final Target savedTarget1 = testdataFactory.createTarget("2911"); @@ -590,7 +614,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { assignDistributionSetWithMaintenanceWindow(ds1.getId(), savedTarget1.getControllerId(), getTestSchedule(10), getTestDuration(10), getTestTimeZone()).getAssignedEntity().iterator().next(); - mvc.perform(get("/default-tenant/controller/v1/2911/")).andExpect(status().isOk()) + mvc.perform(get(CONTROLLER_BASE, "default-tenant", "2911")).andExpect(status().isOk()) .andExpect(jsonPath("$.config.polling.sleep", lessThan("00:05:00"))) .andExpect(jsonPath("$.config.polling.sleep", greaterThanOrEqualTo("00:03:00"))); @@ -599,7 +623,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { assignDistributionSetWithMaintenanceWindow(ds2.getId(), savedTarget2.getControllerId(), getTestSchedule(5), getTestDuration(5), getTestTimeZone()).getAssignedEntity().iterator().next(); - mvc.perform(get("/default-tenant/controller/v1/3911/")).andExpect(status().isOk()) + mvc.perform(get(CONTROLLER_BASE, "default-tenant", "3911")).andExpect(status().isOk()) .andExpect(jsonPath("$.config.polling.sleep", lessThan("00:02:00"))); final Target savedTarget3 = testdataFactory.createTarget("4911"); @@ -607,7 +631,7 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { assignDistributionSetWithMaintenanceWindow(ds3.getId(), savedTarget3.getControllerId(), getTestSchedule(-5), getTestDuration(15), getTestTimeZone()).getAssignedEntity().iterator().next(); - mvc.perform(get("/default-tenant/controller/v1/4911/")).andExpect(status().isOk()) + mvc.perform(get(CONTROLLER_BASE, "default-tenant", "4911")).andExpect(status().isOk()) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:05:00"))); } @@ -620,12 +644,12 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { savedTarget = getFirstAssignedTarget(assignDistributionSetWithMaintenanceWindow(ds.getId(), savedTarget.getControllerId(), getTestSchedule(2), getTestDuration(1), getTestTimeZone())); - mvc.perform(get("/default-tenant/controller/v1/1911/")).andExpect(status().isOk()); + mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")).andExpect(status().isOk()); final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) .getContent().get(0); - mvc.perform(get("/{tenant}/controller/v1/1911/deploymentBase/{actionId}", tenantAware.getCurrentTenant(), + mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), "1911", action.getId()).accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()).andExpect(jsonPath("$.deployment.download", equalTo("forced"))) .andExpect(jsonPath("$.deployment.update", equalTo("skip"))) @@ -640,12 +664,12 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { savedTarget = getFirstAssignedTarget(assignDistributionSetWithMaintenanceWindow(ds.getId(), savedTarget.getControllerId(), getTestSchedule(-5), getTestDuration(10), getTestTimeZone())); - mvc.perform(get("/default-tenant/controller/v1/1911/")).andExpect(status().isOk()); + mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")).andExpect(status().isOk()); final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) .getContent().get(0); - mvc.perform(get("/{tenant}/controller/v1/1911/deploymentBase/{actionId}", tenantAware.getCurrentTenant(), + mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), "1911", action.getId()).accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()).andExpect(jsonPath("$.deployment.download", equalTo("forced"))) .andExpect(jsonPath("$.deployment.update", equalTo("forced"))) @@ -673,15 +697,15 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Description("The system should not create a new target because of a too long controller id.") public void rootRsWithInvalidControllerId() throws Exception { final String invalidControllerId = RandomStringUtils.randomAlphabetic(Target.CONTROLLER_ID_MAX_SIZE + 1); - mvc.perform(get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(), invalidControllerId)) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), invalidControllerId)) .andExpect(status().isBadRequest()); } - public void assertDeploymentActionIsExposedToTarget(final String controllerId, final long expectedActionId) + private void assertDeploymentActionIsExposedToTarget(final String controllerId, final long expectedActionId) throws Exception { final String expectedDeploymentBaseLink = String.format("/%s/controller/v1/%s/deploymentBase/%d", tenantAware.getCurrentTenant(), controllerId, expectedActionId); - mvc.perform(get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(), controllerId) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId) .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(jsonPath("$._links.deploymentBase.href", containsString(expectedDeploymentBaseLink))); diff --git a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/rootcontroller-api-guide.adoc b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/rootcontroller-api-guide.adoc index e7f2b4ebb..849155cb4 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/rootcontroller-api-guide.adoc +++ b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/rootcontroller-api-guide.adoc @@ -15,7 +15,13 @@ toc::[] === Implementation notes -This base resource can be regularly polled by the controller on the provisioning target or device in order to retrieve actions that need to be executed. Those are provided as a list of links to give more detailed information about the action. Links are only available for initial configuration or open actions, respectively. The resource supports Etag based modification checks in order to save traffic. Note: deployments have to be confirmed in order to move on to the next action. Cancellations have to be confirmed or rejected. +This base resource can be regularly polled by the controller on the provisioning target or device in order to retrieve actions that need to be executed. +Those are provided as a list of links to give more detailed information about the action. +Links are only available for initial configuration, open actions, or the latest installed action, respectively. +The resource supports Etag based modification checks in order to save traffic. + +Note: deployments have to be confirmed in order to move on to the next action. +Cancellations have to be confirmed or rejected. === Controller base poll resource @@ -69,7 +75,7 @@ include::../errors/429.adoc[] === Implementation notes -The Hawkbit server might cancel an operation, e.g. an unfinished update has a sucessor. It is up to the provisiong target to decide to accept the cancelation or reject it. +The Hawkbit server might cancel an operation, e.g. an unfinished update has a successor. It is up to the provisioning target to decide to accept the cancelation or reject it. === Cancel an action @@ -109,7 +115,6 @@ include::../errors/429.adoc[] |=== - == POST /{tenant}/controller/v1/{controllerid}/cancelAction/{actionId}/feedback === Implementation notes @@ -151,11 +156,12 @@ include::../errors/415.adoc[] include::../errors/429.adoc[] |=== + == PUT /{tenant}/controller/v1/{controllerid}/configData === Implementation notes -The usual behaviour is that when a new device resgisters at the server it is requested to provide the meta information that will allow the server to identify the device on a hardware level (e.g. hardware revision, mac address, serial number etc.). +The usual behaviour is that when a new device registers at the server it is requested to provide the meta information that will allow the server to identify the device on a hardware level (e.g. hardware revision, mac address, serial number etc.). === Response to a requested metadata pull from the provisioning target device. @@ -258,8 +264,6 @@ include::../errors/429.adoc[] |=== - - == POST /{tenant}/controller/v1/{controllerid}/deploymentBase/{actionId}/feedback @@ -301,6 +305,64 @@ include::../errors/409.adoc[] include::../errors/415.adoc[] include::../errors/429.adoc[] |=== + +== GET /{tenant}/controller/v1/{controllerid}/installedBase/{actionId} + +=== Implementation notes + +Resource to receive information of the previous installation. +Can be used to re-retrieve artifacts of the already finished action, for example in case a re-installation is necessary. +The response will be of the same format as the deploymentBase operation, providing the previous action that has been finished successfully. +As the action is already finished, no further feedback is expected. + +Keep in mind that the provided download links for the artifacts are generated dynamically by the update server. +Host, port and path are not guaranteed to be similar to the provided examples below but will be defined at runtime. + +=== Previously installed action + +==== Curl + +include::{snippets}/rootcontroller/get-controller-installed-base-action/curl-request.adoc[] + +==== Request URL + +include::{snippets}/rootcontroller/get-controller-installed-base-action/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/rootcontroller/get-controller-installed-base-action/path-parameters.adoc[] + +==== Request query parameter + +include::{snippets}/rootcontroller/get-controller-installed-base-action/request-parameters.adoc[] + +=== Response (Status 200) + +==== Response fields + +include::{snippets}/rootcontroller/get-controller-installed-base-action/response-fields.adoc[] + +==== Response example + +The response body includes the detailed operation for the already finished action in the same format as for the deploymentBase operation. + +In this case the (optional) query for the last 10 messages, previously provided by the device, are included. + +include::{snippets}/rootcontroller/get-controller-installed-base-action/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/429.adoc[] +|=== + //// == GET /{tenant}/controller/v1/{controllerid}/softwaremodules @@ -350,7 +412,7 @@ include::../errors/429.adoc[] === Implementation notes -Returns all artifacts whichs is assigned to the software module +Returns all artifacts that are assigned to the software module === Returns artifacts of given software module diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/DdiApiModelProperties.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/DdiApiModelProperties.java index 772ced4a9..80525132e 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/DdiApiModelProperties.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/DdiApiModelProperties.java @@ -92,7 +92,9 @@ final class DdiApiModelProperties { static final String DEPLOYMENT = "Detailed deployment operation"; - static final String CANCEL = "Detailed cancel operation of a deployment."; + static final String CANCEL = "Detailed cancel operation of a deployment"; + + static final String INSTALLED = "Detailed operation of last successfully finished action"; static final String HANDLING_DOWNLOAD = "handling for the download part of the provisioning process ('skip': do not download yet, 'attempt': server asks to download, 'forced': server requests immediate download)"; diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/RootControllerDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/RootControllerDocumentationTest.java index e4333d7bf..294cd4243 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/RootControllerDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/RootControllerDocumentationTest.java @@ -50,7 +50,7 @@ import io.qameta.allure.Story; * Documentation generation for Direct Device Integration API. * */ -@Feature("Documentation Verfication - Direct Device Integration API") +@Feature("Documentation Verification - Direct Device Integration API") @Story("Root Resource") public class RootControllerDocumentationTest extends AbstractApiRestDocumentation { private static final String CONTROLLER_ID = "CONTROLLER_ID"; @@ -67,17 +67,18 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("This base resource can be regularly polled by the controller on the provisioning target or device " - + "in order to retrieve actions that need to be executed. In this case including a config pull request and a deployment. The resource supports Etag based modification " - + "checks in order to save traffic.") + + "in order to retrieve actions that need to be executed. In this case including a config pull request and a deployment. " + + "The resource supports Etag based modification checks in order to save traffic.") @WithUser(tenantId = "TENANT_ID", authorities = "ROLE_CONTROLLER", allSpPermissions = true) public void getControllerBaseWithOpenDeplyoment() throws Exception { - final DistributionSet set = testdataFactory.createDistributionSet("one"); + final Action actionZero = prepareFinishedUpdate(CONTROLLER_ID, "zero", false); + final String controllerId = actionZero.getTarget().getControllerId(); - final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); - assignDistributionSet(set.getId(), target.getControllerId()); + final DistributionSet set = testdataFactory.createDistributionSet("one"); + assignDistributionSet(set.getId(), controllerId); mockMvc.perform(get(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}", - tenantAware.getCurrentTenant(), target.getControllerId()).accept(MediaTypes.HAL_JSON_VALUE)) + tenantAware.getCurrentTenant(), controllerId).accept(MediaTypes.HAL_JSON_VALUE)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(content().contentType(MediaTypes.HAL_JSON)) .andDo(this.document.document( @@ -88,6 +89,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio fieldWithPath("config.polling.sleep").description(DdiApiModelProperties.TARGET_SLEEP), fieldWithPath("_links").description(DdiApiModelProperties.TARGET_OPEN_ACTIONS), fieldWithPath("_links.deploymentBase").description(DdiApiModelProperties.DEPLOYMENT), + fieldWithPath("_links.installedBase").description(DdiApiModelProperties.INSTALLED), fieldWithPath("_links.configData") .description(DdiApiModelProperties.TARGET_CONFIG_DATA)))); } @@ -98,15 +100,17 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio + "Note: as with deployments the cancel action has to be confirmed or rejected in order to move on to the next action.") @WithUser(tenantId = "TENANT_ID", authorities = "ROLE_CONTROLLER", allSpPermissions = true) public void getControllerBaseWithOpenDeploymentCancellation() throws Exception { + final Action actionZero = prepareFinishedUpdate(CONTROLLER_ID, "zero", false); + final String controllerId = actionZero.getTarget().getControllerId(); + final DistributionSet set = testdataFactory.createDistributionSet("one"); final DistributionSet setTwo = testdataFactory.createDistributionSet("two"); - final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); - assignDistributionSet(set.getId(), target.getControllerId()); - assignDistributionSet(setTwo.getId(), target.getControllerId()); + assignDistributionSet(set.getId(), controllerId); + assignDistributionSet(setTwo.getId(), controllerId); mockMvc.perform(get(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}", - tenantAware.getCurrentTenant(), target.getControllerId()).accept(MediaTypes.HAL_JSON_VALUE)) + tenantAware.getCurrentTenant(), controllerId).accept(MediaTypes.HAL_JSON_VALUE)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(content().contentType(MediaTypes.HAL_JSON)) .andDo(this.document.document( @@ -116,7 +120,8 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio fieldWithPath("config.polling").description(DdiApiModelProperties.TARGET_POLL_TIME), fieldWithPath("config.polling.sleep").description(DdiApiModelProperties.TARGET_SLEEP), fieldWithPath("_links").description(DdiApiModelProperties.TARGET_OPEN_ACTIONS), - fieldWithPath("_links.cancelAction").description(DdiApiModelProperties.DEPLOYMENT), + fieldWithPath("_links.cancelAction").description(DdiApiModelProperties.CANCEL), + fieldWithPath("_links.installedBase").description(DdiApiModelProperties.INSTALLED), fieldWithPath("_links.configData") .description(DdiApiModelProperties.TARGET_CONFIG_DATA)))); } @@ -129,7 +134,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio final DistributionSet set = testdataFactory.createDistributionSet("one"); set.getModules().forEach(module -> { - final byte random[] = RandomStringUtils.random(5).getBytes(); + final byte[] random = RandomStringUtils.random(5).getBytes(); artifactManagement.create( new ArtifactUpload(new ByteArrayInputStream(random), module.getId(), "binary.tgz", false, 0)); @@ -163,7 +168,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("It is up to the device to decided how much intermediate feedback is " + "provided. However, the action will be kept open until the controller on the device reports a " - + "finished (either successfull or error) or rejects the oprtioan, e.g. the canceled actions have been started already.") + + "finished (either successful or error) or rejects the operation, e.g. the canceled actions have been started already.") @WithUser(tenantId = "TENANT_ID", authorities = "ROLE_CONTROLLER", allSpPermissions = true) public void postCancelActionFeedback() throws Exception { final DistributionSet set = testdataFactory.createDistributionSet("one"); @@ -201,7 +206,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio } @Test - @Description("The usual behaviour is that when a new device resgisters at the server it is " + @Description("The usual behaviour is that when a new device registers at the server it is " + "requested to provide the meta information that will allow the server to identify the device on a " + "hardware level (e.g. hardware revision, mac address, serial number etc.).") @WithUser(tenantId = "TENANT_ID", authorities = "ROLE_CONTROLLER", allSpPermissions = true) @@ -236,7 +241,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio final DistributionSet set = testdataFactory.createDistributionSet("one"); set.getModules().forEach(module -> { - final byte random[] = RandomStringUtils.random(5).getBytes(); + final byte[] random = RandomStringUtils.random(5).getBytes(); artifactManagement.create( new ArtifactUpload(new ByteArrayInputStream(random), module.getId(), "binary.tgz", false, 0)); @@ -368,7 +373,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Feedback channel. It is up to the device to decided how much intermediate feedback is " + "provided. However, the action will be kept open until the controller on the device reports a " - + "finished (either successfull or error).") + + "finished (either successful or error).") @WithUser(tenantId = "TENANT_ID", authorities = "ROLE_CONTROLLER", allSpPermissions = true) public void postBasedeploymentActionFeedback() throws Exception { final DistributionSet set = testdataFactory.createDistributionSet("one"); @@ -408,15 +413,15 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio } @Test - @Description("Returns all artifacts whichs is assigned to the software module." - + "Can be usesfull for the target to double check that its current state matches with the targeted state.") + @Description("Returns all artifacts that are assigned to the software module." + + "Can be useful for the target to double check that its current state matches with the targeted state.") @WithUser(tenantId = "TENANT_ID", authorities = "ROLE_CONTROLLER", allSpPermissions = true) public void getSoftwareModulesArtifacts() throws Exception { final DistributionSet set = testdataFactory.createDistributionSet(""); final SoftwareModule module = (SoftwareModule) set.getModules().toArray()[0]; - final byte random[] = RandomStringUtils.random(5).getBytes(); + final byte[] random = RandomStringUtils.random(5).getBytes(); artifactManagement .create(new ArtifactUpload(new ByteArrayInputStream(random), module.getId(), "binaryFile", false, 0)); @@ -450,4 +455,103 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio .description(DdiApiModelProperties.ARTIFACT_HTTP_HASHES_MD5SUM_LINK)))); } + @Test + @Description("Resource to receive information of the previous installation. The response will be of same format as " + + "the deploymentBase operation.") + @WithUser(tenantId = "TENANT_ID", authorities = "ROLE_CONTROLLER", allSpPermissions = true) + public void getControllerInstalledBaseAction() throws Exception { + final DistributionSet set = testdataFactory.createDistributionSet("zero"); + + set.getModules().forEach(module -> { + final byte[] random = RandomStringUtils.random(5).getBytes(); + artifactManagement.create( + new ArtifactUpload(new ByteArrayInputStream(random), module.getId(), "binary.tgz", false, 0)); + artifactManagement.create( + new ArtifactUpload(new ByteArrayInputStream(random), module.getId(), "file.signature", false, 0)); + }); + + softwareModuleManagement.createMetaData( + entityFactory.softwareModuleMetadata().create(set.getModules().iterator().next().getId()) + .key("aMetadataKey").value("Metadata value as defined in software module").targetVisible(true)); + + final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID)); + final Long actionId = getFirstAssignedActionId(assignDistributionSetWithMaintenanceWindow(set.getId(), + target.getControllerId(), getTestSchedule(-5), getTestDuration(10), getTestTimeZone())); + + controllerManagement.addInformationalActionStatus( + entityFactory.actionStatus().create(actionId).message("Started download").status(Status.DOWNLOAD)); + controllerManagement.addInformationalActionStatus(entityFactory.actionStatus().create(actionId) + .message("Download failed. ErrorCode #5876745. Retry").status(Status.WARNING)); + controllerManagement.addInformationalActionStatus( + entityFactory.actionStatus().create(actionId).message("Download done").status(Status.DOWNLOADED)); + controllerManagement.addInformationalActionStatus( + entityFactory.actionStatus().create(actionId).message("Write firmware").status(Status.RUNNING)); + controllerManagement.addInformationalActionStatus( + entityFactory.actionStatus().create(actionId).message("Reboot").status(Status.RUNNING)); + controllerManagement.addUpdateActionStatus( + entityFactory.actionStatus().create(actionId).message("Installed").status(Status.FINISHED)); + + mockMvc.perform(get( + DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/" + DdiRestConstants.INSTALLED_BASE_ACTION + + "/{actionId}?actionHistory=10", + tenantAware.getCurrentTenant(), target.getControllerId(), actionId).accept(MediaTypes.HAL_JSON_VALUE)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andDo(this.document.document( + pathParameters(parameterWithName("tenant").description(ApiModelPropertiesGeneric.TENANT), + parameterWithName("controllerId").description(DdiApiModelProperties.CONTROLLER_ID), + parameterWithName("actionId").description(DdiApiModelProperties.ACTION_ID)), + requestParameters( + parameterWithName("actionHistory").description(DdiApiModelProperties.ACTION_HISTORY)), + responseFields(fieldWithPath("id").description(DdiApiModelProperties.ACTION_ID), + fieldWithPath("deployment").description(DdiApiModelProperties.DEPLOYMENT), + fieldWithPath("deployment.download") + .description(DdiApiModelProperties.HANDLING_DOWNLOAD).type("enum") + .attributes(key("value").value("['skip', 'attempt', 'forced']")), + fieldWithPath("deployment.update").description(DdiApiModelProperties.HANDLING_UPDATE) + .type("enum").attributes(key("value").value("['skip', 'attempt', 'forced']")), + fieldWithPath("deployment.maintenanceWindow") + .description(DdiApiModelProperties.MAINTENANCE_WINDOW).type("enum") + .attributes(key("value").value("['available', 'unavailable']")), + fieldWithPath("deployment.chunks").description(DdiApiModelProperties.CHUNK), + fieldWithPath("deployment.chunks[].metadata") + .description(DdiApiModelProperties.CHUNK_META_DATA).optional(), + fieldWithPath("deployment.chunks[].metadata[].key") + .description(DdiApiModelProperties.CHUNK_META_DATA_KEY).optional(), + fieldWithPath("deployment.chunks[].metadata[].value") + .description(DdiApiModelProperties.CHUNK_META_DATA_VALUE).optional(), + fieldWithPath("deployment.chunks[].part").description(DdiApiModelProperties.CHUNK_TYPE), + fieldWithPath("deployment.chunks[].name").description(DdiApiModelProperties.CHUNK_NAME), + fieldWithPath("deployment.chunks[].version") + .description(DdiApiModelProperties.CHUNK_VERSION), + fieldWithPath("deployment.chunks[].artifacts") + .description(DdiApiModelProperties.ARTIFACTS), + fieldWithPath("deployment.chunks[].artifacts[].filename") + .description(DdiApiModelProperties.ARTIFACTS), + fieldWithPath("deployment.chunks[].artifacts[].hashes") + .description(DdiApiModelProperties.ARTIFACTS), + fieldWithPath("deployment.chunks[].artifacts[].hashes.sha1") + .description(DdiApiModelProperties.ARTIFACT_HASHES_SHA1), + fieldWithPath("deployment.chunks[].artifacts[].hashes.md5") + .description(DdiApiModelProperties.ARTIFACT_HASHES_MD5), + fieldWithPath("deployment.chunks[].artifacts[].hashes.sha256") + .description(DdiApiModelProperties.ARTIFACT_HASHES_SHA256), + fieldWithPath("deployment.chunks[].artifacts[].size") + .description(DdiApiModelProperties.ARTIFACT_SIZE), + fieldWithPath("deployment.chunks[].artifacts[]._links.download") + .description(DdiApiModelProperties.ARTIFACT_HTTPS_DOWNLOAD_LINK_BY_CONTROLLER), + fieldWithPath("deployment.chunks[].artifacts[]._links.md5sum") + .description(DdiApiModelProperties.ARTIFACT_HTTPS_HASHES_MD5SUM_LINK), + fieldWithPath("deployment.chunks[].artifacts[]._links.download-http") + .description(DdiApiModelProperties.ARTIFACT_HTTP_DOWNLOAD_LINK_BY_CONTROLLER), + fieldWithPath("deployment.chunks[].artifacts[]._links.md5sum-http") + .description(DdiApiModelProperties.ARTIFACT_HTTP_HASHES_MD5SUM_LINK), + fieldWithPath("actionHistory").description(DdiApiModelProperties.ACTION_HISTORY_RESP), + fieldWithPath("actionHistory.status") + .description(DdiApiModelProperties.ACTION_HISTORY_RESP_STATUS), + fieldWithPath("actionHistory.messages") + .description(DdiApiModelProperties.ACTION_HISTORY_RESP_MESSAGES)))); + + } + }