diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java index e4a079bf3..3c8f28596 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java @@ -8,9 +8,12 @@ */ package org.eclipse.hawkbit.repository; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + /** * Sort fields for {@link ActionRest}. - * */ public enum ActionFields implements FieldNameProvider, FieldValueConverter { @@ -18,22 +21,65 @@ public enum ActionFields implements FieldNameProvider, FieldValueConverter subEntityAttributes; + private ActionFields(final String fieldName) { this.fieldName = fieldName; + this.subEntityAttributes = Collections.emptyList(); + } + + private ActionFields(final String fieldName, final String... subEntityAttributes) { + this.fieldName = fieldName; + this.subEntityAttributes = Arrays.asList(subEntityAttributes); + } + + @Override + public List getSubEntityAttributes() { + return subEntityAttributes; } @Override @@ -49,6 +95,14 @@ public enum ActionFields implements FieldNameProvider, FieldValueConverter findActionsAll(@NotNull Pageable pageable); + /** + * Retrieves all {@link Action} entities which match the given RSQL query. + * + * @param rsqlParam + * RSQL query string + * @param pageable + * the page request parameter for paging and sorting the result + * + * @return a paged list of {@link Action}s. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + Slice findActions(@NotNull String rsqlParam, @NotNull Pageable pageable); + /** * Retrieves all {@link Action} which assigned to a specific * {@link DistributionSet}. diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java index 575c4f12c..c7c959d20 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java @@ -72,8 +72,6 @@ import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType; import org.eclipse.hawkbit.repository.model.DistributionSetType; -import org.eclipse.hawkbit.repository.model.Rollout; -import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetType; @@ -582,7 +580,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl if (rolloutGroupActions.getContent().isEmpty()) { return 0L; } - + final List newTargetAssignments = handleTargetAssignments(rolloutGroupActions); if (!newTargetAssignments.isEmpty()) { @@ -592,7 +590,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl return rolloutGroupActions.getTotalElements(); }); } - + private List handleTargetAssignments(final Page rolloutGroupActions) { // Close actions already assigned and collect pending assignments final List pendingTargetAssignments = rolloutGroupActions.getContent().stream() @@ -651,9 +649,9 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl return Collections.unmodifiableList(savedActions); } - private void closeOrCancelOpenDeviceActions(final List actions){ + private void closeOrCancelOpenDeviceActions(final List actions) { final List targetIds = actions.stream().map(JpaAction::getTarget).map(Target::getId) - .collect(Collectors.toList()); + .collect(Collectors.toList()); if (isActionsAutocloseEnabled()) { onlineDsAssignmentStrategy.closeObsoleteUpdateActions(targetIds); } else { @@ -661,7 +659,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl } } - private List activateActions(final List actions){ + private List activateActions(final List actions) { actions.forEach(action -> { action.setActive(true); action.setStatus(Status.RUNNING); @@ -860,6 +858,13 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl return actionRepository.count(); } + @Override + public long countActions(final String rsqlParam) { + final List> specList = Arrays.asList( + RSQLUtility.buildRsqlSpecification(rsqlParam, ActionFields.class, virtualPropertyReplacer, database)); + return JpaManagementHelper.countBySpec(actionRepository, specList); + } + @Override public long countActionsByDistributionSetIdAndActiveIsTrue(final Long distributionSet) { return actionRepository.countByDistributionSetIdAndActiveIsTrue(distributionSet); @@ -882,6 +887,13 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl return JpaManagementHelper.findAllWithoutCountBySpec(actionRepository, pageable, null); } + @Override + public Slice findActions(final String rsqlParam, final Pageable pageable) { + final List> specList = Arrays.asList( + RSQLUtility.buildRsqlSpecification(rsqlParam, ActionFields.class, virtualPropertyReplacer, database)); + return JpaManagementHelper.findAllWithoutCountBySpec(actionRepository, pageable, specList); + } + @Override public Optional getAssignedDistributionSet(final String controllerId) { throwExceptionIfTargetDoesNotExist(controllerId); @@ -990,4 +1002,5 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl }); } } + } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java index e37ddbe9a..271f2113b 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java @@ -54,6 +54,9 @@ public class MgmtAction extends MgmtBaseEntity { @JsonProperty private String status; + @JsonProperty + private String detailStatus; + @JsonProperty private Long forceTime; @@ -65,7 +68,7 @@ public class MgmtAction extends MgmtBaseEntity { @JsonProperty private MgmtMaintenanceWindow maintenanceWindow; - + @JsonProperty private Long rollout; @@ -132,7 +135,7 @@ public class MgmtAction extends MgmtBaseEntity { return rollout; } - public void setRollout(Long rollout) { + public void setRollout(final Long rollout) { this.rollout = rollout; } @@ -140,8 +143,16 @@ public class MgmtAction extends MgmtBaseEntity { return rolloutName; } - public void setRolloutName(String rolloutName) { + public void setRolloutName(final String rolloutName) { this.rolloutName = rolloutName; } + public String getDetailStatus() { + return detailStatus; + } + + public void setDetailStatus(final String detailStatus) { + this.detailStatus = detailStatus; + } + } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtActionRestApi.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtActionRestApi.java new file mode 100644 index 000000000..8ebcd4137 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtActionRestApi.java @@ -0,0 +1,57 @@ +/** + * 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.mgmt.rest.api; + +import org.eclipse.hawkbit.mgmt.json.model.PagedList; +import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction; +import org.springframework.hateoas.MediaTypes; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * REST API providing (read-only) access to actions. + */ +@RequestMapping(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING) +public interface MgmtActionRestApi { + + /** + * Handles the GET request of retrieving all actions. + * + * @param pagingOffsetParam + * the offset of list of actions for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=distributionSet.id==1} + * @param representationModeParam + * the representation mode parameter specifying whether a compact + * or a full representation shall be returned + * @return a list of all actions for a defined or default page request with + * status OK. The response is always paged. In any failure the + * JsonResponseExceptionHandler is handling the response. + */ + + @GetMapping(produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity> getActions( + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) int pagingOffsetParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) int pagingLimitParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) String sortParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) String rsqlParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_REPRESENTATION_MODE, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_REPRESENTATION_MODE_DEFAULT) String representationModeParam); + +} \ No newline at end of file diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRepresentationMode.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRepresentationMode.java new file mode 100644 index 000000000..c53a72e5d --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRepresentationMode.java @@ -0,0 +1,38 @@ +/** + * 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.mgmt.rest.api; + +import java.util.Arrays; +import java.util.Optional; + +/** + * Enumeration of the supported representation modes. + */ +public enum MgmtRepresentationMode { + + FULL("full"), + + COMPACT("compact"); + + private final String mode; + + private MgmtRepresentationMode(final String mode) { + this.mode = mode; + } + + @Override + public String toString() { + return mode; + } + + public static Optional fromValue(final String value) { + return Arrays.stream(MgmtRepresentationMode.values()).filter(v -> v.mode.equalsIgnoreCase(value)).findFirst(); + } + +} diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRestConstants.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRestConstants.java index 69fb14eb9..698180002 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRestConstants.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRestConstants.java @@ -63,7 +63,7 @@ public final class MgmtRestConstants { /** * The target URL mapping, href link for assigned target type. */ - public static final String TARGET_V1_ASSIGNED_TARGET_TYPE= "targetType"; + public static final String TARGET_V1_ASSIGNED_TARGET_TYPE = "targetType"; /** * The target URL mapping, href link for assigned distribution set. @@ -140,6 +140,11 @@ public final class MgmtRestConstants { */ public static final String DISTRIBUTIONSET_TAG_DISTRIBUTIONSETS_REQUEST_MAPPING = "/{distributionsetTagId}/assigned"; + /** + * The action URL mapping rest resource. + */ + public static final String ACTION_V1_REQUEST_MAPPING = BASE_V1_REQUEST_MAPPING + "/actions"; + /** * The default offset parameter in case the offset parameter is not present * in the request. @@ -186,6 +191,17 @@ public final class MgmtRestConstants { */ public static final String REQUEST_PARAMETER_SEARCH = "q"; + /** + * The request parameter for specifying the representation mode. The value + * of this parameter can either be "full" or "compact". + */ + public static final String REQUEST_PARAMETER_REPRESENTATION_MODE = "representation"; + + /** + * The default representation mode. + */ + public static final String REQUEST_PARAMETER_REPRESENTATION_MODE_DEFAULT = "compact"; + /** * The software module type URL mapping rest resource. */ diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java index 1a5abd42e..a24b82654 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java @@ -45,16 +45,20 @@ public interface MgmtRolloutRestApi { * @param rsqlParam * the search parameter in the request URL, syntax * {@code q=name==abc} + * @param representationModeParam + * the representation mode parameter specifying whether a compact + * or a full representation shall be returned * @return a list of all rollouts for a defined or default page request with * status OK. The response is always paged. In any failure the * JsonResponseExceptionHandler is handling the response. */ @GetMapping(produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) ResponseEntity> getRollouts( - @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) int pagingOffsetParam, - @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) int pagingLimitParam, - @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) String sortParam, - @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) String rsqlParam); + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_REPRESENTATION_MODE, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_REPRESENTATION_MODE_DEFAULT) String representationModeParam); /** * Handles the GET request of retrieving a single rollout. diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtActionMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtActionMapper.java new file mode 100644 index 000000000..51a826b73 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtActionMapper.java @@ -0,0 +1,57 @@ +/** + * 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.mgmt.rest.resource; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtRepresentationMode; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.rest.data.ResponseList; + +/** + * A mapper which maps repository model to RESTful model representation and + * back. + */ +public final class MgmtActionMapper { + + private MgmtActionMapper() { + // Utility class + } + + /** + * Create a response for actions. + * + * @param actions + * list of actions + * @param repMode + * the representation mode + * + * @return the response + */ + public static List toResponse(final Collection actions, final MgmtRepresentationMode repMode) { + if (actions == null) { + return Collections.emptyList(); + } + return new ResponseList<>(actions.stream().map(action -> MgmtActionMapper.toResponse(action, repMode)) + .collect(Collectors.toList())); + } + + static MgmtAction toResponse(final Action action, final MgmtRepresentationMode repMode) { + final String controllerId = action.getTarget().getControllerId(); + if (repMode == MgmtRepresentationMode.COMPACT) { + return MgmtTargetMapper.toResponse(controllerId, action); + } + return MgmtTargetMapper.toResponseWithLinks(controllerId, action); + } + +} diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtActionResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtActionResource.java new file mode 100644 index 000000000..7a5326dc1 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtActionResource.java @@ -0,0 +1,70 @@ +/** + * 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.mgmt.rest.resource; + +import org.eclipse.hawkbit.mgmt.json.model.PagedList; +import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtActionRestApi; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtRepresentationMode; +import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; +import org.eclipse.hawkbit.repository.model.Action; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@ConditionalOnProperty(name = "hawkbit.rest.MgmtActionResource.enabled", matchIfMissing = true) +public class MgmtActionResource implements MgmtActionRestApi { + + private static final Logger LOG = LoggerFactory.getLogger(MgmtActionResource.class); + + private final DeploymentManagement deploymentManagement; + + MgmtActionResource(final DeploymentManagement deploymentManagement) { + this.deploymentManagement = deploymentManagement; + } + + @Override + public ResponseEntity> getActions(final int pagingOffsetParam, final int pagingLimitParam, + final String sortParam, final String rsqlParam, final String representationModeParam) { + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeActionSortParam(sortParam); + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + + final Slice actions; + final Long totalActionCount; + if (rsqlParam != null) { + actions = this.deploymentManagement.findActions(rsqlParam, pageable); + totalActionCount = this.deploymentManagement.countActions(rsqlParam); + } else { + actions = this.deploymentManagement.findActionsAll(pageable); + totalActionCount = this.deploymentManagement.countActionsAll(); + } + + final MgmtRepresentationMode repMode = MgmtRepresentationMode.fromValue(representationModeParam) + .orElseGet(() -> { + // no need for a 400, just apply a safe fallback + LOG.warn("Received an invalid representation mode: {}", representationModeParam); + return MgmtRepresentationMode.COMPACT; + }); + + return ResponseEntity + .ok(new PagedList<>(MgmtActionMapper.toResponse(actions.getContent(), repMode), totalActionCount)); + + } + +} diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java index 04e99b6bf..8866404a4 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java @@ -26,6 +26,7 @@ import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutSuccessAction; import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutSuccessAction.SuccessAction; import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroup; import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroupResponseBody; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi; import org.eclipse.hawkbit.repository.EntityFactory; @@ -57,11 +58,15 @@ final class MgmtRolloutMapper { } static List toResponseRollout(final List rollouts) { + return toResponseRollout(rollouts, false); + } + + static List toResponseRollout(final List rollouts, final boolean withDetails) { if (rollouts == null) { return Collections.emptyList(); } - return rollouts.stream().map(rollout -> toResponseRollout(rollout, false)).collect(Collectors.toList()); + return rollouts.stream().map(rollout -> toResponseRollout(rollout, withDetails)).collect(Collectors.toList()); } static MgmtRolloutResponseBody toResponseRollout(final Rollout rollout, final boolean withDetails) { @@ -95,6 +100,10 @@ final class MgmtRolloutMapper { body.add(linkTo(methodOn(MgmtRolloutRestApi.class).getRolloutGroups(rollout.getId(), MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET_VALUE, MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE, null, null)).withRel("groups")); + + final DistributionSet distributionSet = rollout.getDistributionSet(); + body.add(linkTo(methodOn(MgmtDistributionSetRestApi.class).getDistributionSet(distributionSet.getId())) + .withRel("distributionset").withName(distributionSet.getName() + ":" + distributionSet.getVersion())); } body.add(linkTo(methodOn(MgmtRolloutRestApi.class).getRollout(rollout.getId())).withSelfRel()); diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java index 06000715f..3548ee085 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java @@ -18,6 +18,7 @@ import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutResponseBody; import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBody; import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroupResponseBody; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtRepresentationMode; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi; import org.eclipse.hawkbit.repository.DistributionSetManagement; @@ -35,6 +36,8 @@ import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; import org.eclipse.hawkbit.repository.model.Target; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -51,6 +54,9 @@ import org.springframework.web.bind.annotation.RestController; */ @RestController public class MgmtRolloutResource implements MgmtRolloutRestApi { + + private static final Logger LOG = LoggerFactory.getLogger(MgmtRolloutResource.class); + private final RolloutManagement rolloutManagement; private final RolloutGroupManagement rolloutGroupManagement; @@ -76,14 +82,14 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi { @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, - @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam, + final String representationModeParam) { final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); final Sort sorting = PagingUtility.sanitizeRolloutSortParam(sortParam); final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); - final Page findRolloutsAll; if (rsqlParam != null) { findRolloutsAll = this.rolloutManagement.findByRsql(pageable, rsqlParam, false); @@ -91,7 +97,15 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi { findRolloutsAll = this.rolloutManagement.findAll(pageable, false); } - final List rest = MgmtRolloutMapper.toResponseRollout(findRolloutsAll.getContent()); + final MgmtRepresentationMode repMode = MgmtRepresentationMode.fromValue(representationModeParam) + .orElseGet(() -> { + // no need for a 400, just apply a safe fallback + LOG.warn("Received an invalid representation mode: {}", representationModeParam); + return MgmtRepresentationMode.COMPACT; + }); + + final List rest = MgmtRolloutMapper.toResponseRollout(findRolloutsAll.getContent(), + repMode == MgmtRepresentationMode.FULL); return ResponseEntity.ok(new PagedList<>(rest, findRolloutsAll.getTotalElements())); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java index 87ba335d7..75c7b851f 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java @@ -39,6 +39,7 @@ import org.eclipse.hawkbit.repository.builder.TargetCreate; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.MetaData; import org.eclipse.hawkbit.repository.model.PollStatus; import org.eclipse.hawkbit.repository.model.Rollout; @@ -226,6 +227,8 @@ public final class MgmtTargetMapper { result.setStatus(MgmtAction.ACTION_FINISHED); } + result.setDetailStatus(action.getStatus().toString().toLowerCase()); + final Rollout rollout = action.getRollout(); if (rollout != null) { result.setRollout(rollout.getId()); @@ -256,9 +259,13 @@ public final class MgmtTargetMapper { result.add(linkTo(methodOn(MgmtTargetRestApi.class).getAction(controllerId, action.getId())) .withRel(MgmtRestConstants.TARGET_V1_CANCELED_ACTION)); } - result.add(linkTo( - methodOn(MgmtDistributionSetRestApi.class).getDistributionSet(action.getDistributionSet().getId())) - .withRel("distributionset")); + + result.add(linkTo(methodOn(MgmtTargetRestApi.class).getTarget(controllerId)).withRel("target") + .withName(action.getTarget().getName())); + + final DistributionSet distributionSet = action.getDistributionSet(); + result.add(linkTo(methodOn(MgmtDistributionSetRestApi.class).getDistributionSet(distributionSet.getId())) + .withRel("distributionset").withName(distributionSet.getName() + ":" + distributionSet.getVersion())); result.add(linkTo(methodOn(MgmtTargetRestApi.class).getActionStatusList(controllerId, action.getId(), 0, MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE, @@ -268,7 +275,7 @@ public final class MgmtTargetMapper { final Rollout rollout = action.getRollout(); if (rollout != null) { result.add(linkTo(methodOn(MgmtRolloutRestApi.class).getRollout(rollout.getId())) - .withRel(MgmtRestConstants.TARGET_V1_ROLLOUT)); + .withRel(MgmtRestConstants.TARGET_V1_ROLLOUT).withName(rollout.getName())); } return result; diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtActionResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtActionResourceTest.java new file mode 100644 index 000000000..b19013d38 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtActionResourceTest.java @@ -0,0 +1,421 @@ +/** + * 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.mgmt.rest.resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +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.Iterator; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.awaitility.Awaitility; +import org.awaitility.Duration; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtActionRestApi; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtRepresentationMode; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.Rollout; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; +import org.junit.jupiter.api.Test; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; + +/** + * Integration test for the {@link MgmtActionRestApi}. + */ +@Feature("Component Tests - Management API") +@Story("Action Resource") +class MgmtActionResourceTest extends AbstractManagementApiIntegrationTest { + + private static final String JSON_PATH_ROOT = "$"; + private static final String JSON_PATH_FIELD_CONTENT = ".content"; + private static final String JSON_PATH_FIELD_SIZE = ".size"; + private static final String JSON_PATH_FIELD_TOTAL = ".total"; + + private static final String JSON_PATH_PAGED_LIST_CONTENT = JSON_PATH_ROOT + JSON_PATH_FIELD_CONTENT; + private static final String JSON_PATH_PAGED_LIST_SIZE = JSON_PATH_ROOT + JSON_PATH_FIELD_SIZE; + private static final String JSON_PATH_PAGED_LIST_TOTAL = JSON_PATH_ROOT + JSON_PATH_FIELD_TOTAL; + + @Test + @Description("Verifies that actions can be filtered based on action status.") + void filterActionsByStatus() throws Exception { + + // prepare test + final DistributionSet dsA = testdataFactory.createDistributionSet(""); + assignDistributionSet(dsA, Collections.singletonList(testdataFactory.createTarget("knownTargetId"))); + + final String rsqlPendingStatus = "status==pending"; + final String rsqlFinishedStatus = "status==finished"; + final String rsqlPendingOrFinishedStatus = rsqlFinishedStatus + "," + rsqlPendingStatus; + + // pending status one result + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlPendingStatus)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))).andExpect(jsonPath("content[0].status", equalTo("pending"))); + + // finished status none result + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlFinishedStatus)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(0))) + .andExpect(jsonPath("size", equalTo(0))); + + // pending or finished status one result + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlPendingOrFinishedStatus)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))).andExpect(jsonPath("content[0].status", equalTo("pending"))); + + } + + @Test + @Description("Verifies that actions can be filtered based on the detailed action status.") + void filterActionsByDetailStatus() throws Exception { + + // prepare test + final DistributionSet dsA = testdataFactory.createDistributionSet(""); + assignDistributionSet(dsA, Collections.singletonList(testdataFactory.createTarget("knownTargetId"))); + + final String rsqlPendingStatus = "detailStatus==running"; + final String rsqlFinishedStatus = "detailStatus==finished"; + final String rsqlPendingOrFinishedStatus = rsqlFinishedStatus + "," + rsqlPendingStatus; + + // running status one result + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlPendingStatus)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))) + .andExpect(jsonPath("content[0].detailStatus", equalTo("running"))) + .andExpect(jsonPath("content[0].status", equalTo("pending"))); + + // finished status none result + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlFinishedStatus)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(0))) + .andExpect(jsonPath("size", equalTo(0))); + + // running or finished status one result + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlPendingOrFinishedStatus)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))) + .andExpect(jsonPath("content[0].detailStatus", equalTo("running"))) + .andExpect(jsonPath("content[0].status", equalTo("pending"))); + + } + + @Test + @Description("Verifies that actions can be filtered based on distribution set fields.") + void filterActionsByDistributionSet() throws Exception { + + // prepare test + final DistributionSet ds = testdataFactory.createDistributionSet(""); + assignDistributionSet(ds, Collections.singletonList(testdataFactory.createTarget("knownTargetId"))); + + final String rsqlDsName = "distributionSet.name==" + ds.getName() + "*"; + final String rsqlDsVersion = "distributionSet.version==" + ds.getVersion(); + final String rsqlDsId = "distributionSet.id==" + ds.getId(); + + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlDsName) + .param(MgmtRestConstants.REQUEST_PARAMETER_REPRESENTATION_MODE, MgmtRepresentationMode.FULL.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))).andExpect(jsonPath("content.[0]._links.distributionset.name", + equalTo(ds.getName() + ":" + ds.getVersion()))); + + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlDsVersion)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))); + + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlDsName + "," + rsqlDsVersion)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))); + + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=distributionSet.name==FooBar")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(0))) + .andExpect(jsonPath("size", equalTo(0))); + + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlDsId)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))); + } + + @Test + @Description("Verifies that actions can be filtered based on rollout fields.") + void filterActionsByRollout() throws Exception { + + // prepare test + final DistributionSet ds = testdataFactory.createDistributionSet(); + final Target target0 = testdataFactory.createTarget("t0"); + + // manual assignment + assignDistributionSet(ds, Collections.singletonList(target0)); + + // rollout + final Target target1 = testdataFactory.createTarget("t1"); + final Rollout rollout = testdataFactory.createRolloutByVariables("TestRollout", "TestDesc", 1, + "name==" + target1.getName(), ds, "50", "5"); + rolloutManagement.start(rollout.getId()); + rolloutManagement.handleRollouts(); + + final String rsqlRolloutName = "rollout.name==" + rollout.getName(); + final String rsqlRolloutId = "rollout.id==" + rollout.getId(); + + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlRolloutName) + .param(MgmtRestConstants.REQUEST_PARAMETER_REPRESENTATION_MODE, MgmtRepresentationMode.FULL.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))) + .andExpect(jsonPath("content.[0]._links.target.name", equalTo(target1.getName()))).andExpect(jsonPath( + "content.[0]._links.distributionset.name", equalTo(ds.getName() + ":" + ds.getVersion()))); + + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlRolloutId) + .param(MgmtRestConstants.REQUEST_PARAMETER_REPRESENTATION_MODE, MgmtRepresentationMode.FULL.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))) + .andExpect(jsonPath("content.[0]._links.target.name", equalTo(target1.getName()))).andExpect(jsonPath( + "content.[0]._links.distributionset.name", equalTo(ds.getName() + ":" + ds.getVersion()))); + } + + @Test + @Description("Verifies that actions can be filtered based on target fields.") + void filterActionsByTarget() throws Exception { + + // prepare test + final Target target = testdataFactory.createTarget("knownTargetId"); + final DistributionSet ds = testdataFactory.createDistributionSet(""); + assignDistributionSet(ds, Collections.singletonList(target)); + + final String rsqlTargetName = "target.name==knownTargetId"; + + // pending status one result + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?q=" + rsqlTargetName) + .param(MgmtRestConstants.REQUEST_PARAMETER_REPRESENTATION_MODE, MgmtRepresentationMode.FULL.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))) + .andExpect(jsonPath("content.[0]._links.target.name", equalTo(target.getName()))).andExpect(jsonPath( + "content.[0]._links.distributionset.name", equalTo(ds.getName() + ":" + ds.getVersion()))); + } + + @Test + @Description("Verifies that all available actions are returned if the complete collection is requested.") + void getActions() throws Exception { + final String knownTargetId = "targetId"; + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + + final Action action0 = actions.get(0); + final Action action1 = actions.get(1); + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING).param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, + "ID:ASC")).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + + // verify action 1 + .andExpect(jsonPath("content.[1].id", equalTo(action1.getId().intValue()))) + .andExpect(jsonPath("content.[1].type", equalTo("update"))) + .andExpect(jsonPath("content.[1].status", equalTo("pending"))) + .andExpect(jsonPath("content.[1].detailStatus", equalTo("running"))) + .andExpect(jsonPath("content.[1]._links.self.href", + equalTo(generateActionLink(knownTargetId, action1.getId())))) + + // verify action 0 + .andExpect(jsonPath("content.[0].id", equalTo(action0.getId().intValue()))) + .andExpect(jsonPath("content.[0].type", equalTo("cancel"))) + .andExpect(jsonPath("content.[0].status", equalTo("pending"))) + .andExpect(jsonPath("content.[1].detailStatus", equalTo("running"))) + .andExpect(jsonPath("content.[0]._links.self.href", + equalTo(generateActionLink(knownTargetId, action0.getId())))) + + // verify collection properties + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(2))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(2))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(2))); + } + + @Test + @Description("Verifies that a full representation of all actions is returned if the collection is requested for representation mode 'full'.") + void getActionsFullRepresentation() throws Exception { + final String knownTargetId = "targetId"; + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + + final Action action0 = actions.get(0); + final Action action1 = actions.get(1); + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING) + .param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC") + .param(MgmtRestConstants.REQUEST_PARAMETER_REPRESENTATION_MODE, MgmtRepresentationMode.FULL.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + + // verify action 1 + .andExpect(jsonPath("content.[1].id", equalTo(action1.getId().intValue()))) + .andExpect(jsonPath("content.[1].type", equalTo("update"))) + .andExpect(jsonPath("content.[1].status", equalTo("pending"))) + .andExpect(jsonPath("content.[1].detailStatus", equalTo("running"))) + .andExpect(jsonPath("content.[1]._links.self.href", + equalTo(generateActionLink(knownTargetId, action1.getId())))) + .andExpect(jsonPath("content.[1]._links.target.href", equalTo(generateTargetLink(knownTargetId)))) + .andExpect(jsonPath("content.[1]._links.distributionset.href", + equalTo(generateDistributionSetLink(action1)))) + + // verify action 0 + .andExpect(jsonPath("content.[0].id", equalTo(action0.getId().intValue()))) + .andExpect(jsonPath("content.[0].type", equalTo("cancel"))) + .andExpect(jsonPath("content.[0].status", equalTo("pending"))) + .andExpect(jsonPath("content.[0].detailStatus", equalTo("canceling"))) + .andExpect(jsonPath("content.[0]._links.self.href", + equalTo(generateActionLink(knownTargetId, action0.getId())))) + .andExpect(jsonPath("content.[0]._links.target.href", equalTo(generateTargetLink(knownTargetId)))) + .andExpect(jsonPath("content.[0]._links.distributionset.href", + equalTo(generateDistributionSetLink(action0)))) + + // verify collection properties + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(2))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(2))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(2))); + } + + @Test + @Description("Verifies that the get request for actions returns an empty collection if no assignments have been done yet.") + void getActionsWithEmptyResult() throws Exception { + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("size", equalTo(0))) + .andExpect(jsonPath("content", hasSize(0))).andExpect(jsonPath("total", equalTo(0))); + } + + @Test + @Description("Verifies paging is respected as expected.") + void getMultipleActionsWithPagingLimitRequestParameter() throws Exception { + final String knownTargetId = "targetId"; + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + + // page 1: one entry + final Action action0 = actions.get(0); + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING) + .param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(1)) + .param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + + // verify action 0 + .andExpect(jsonPath("content.[0].id", equalTo(action0.getId().intValue()))) + .andExpect(jsonPath("content.[0].type", equalTo("cancel"))) + .andExpect(jsonPath("content.[0].status", equalTo("pending"))) + .andExpect(jsonPath("content.[0].detailStatus", equalTo(action0.getStatus().toString().toLowerCase()))) + .andExpect(jsonPath("content.[0]._links.self.href", + equalTo(generateActionLink(knownTargetId, action0.getId())))) + + // verify collection properties + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(2))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(1))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(1))); + + // page 2: one entry + final Action action1 = actions.get(1); + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING) + .param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(1)) + .param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, String.valueOf(1)) + .param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, String.valueOf(1)) + .param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + + // verify action 1 + .andExpect(jsonPath("content.[0].id", equalTo(action1.getId().intValue()))) + .andExpect(jsonPath("content.[0].type", equalTo("update"))) + .andExpect(jsonPath("content.[0].status", equalTo("pending"))) + .andExpect(jsonPath("content.[0].detailStatus", equalTo(action1.getStatus().toString().toLowerCase()))) + .andExpect(jsonPath("content.[0]._links.self.href", + equalTo(generateActionLink(knownTargetId, action1.getId())))) + + // verify collection properties + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(2))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(1))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(1))); + } + + @Test + @Description("Verifies that the actions resource is read-only.") + void invalidRequestsOnActionResource() throws Exception { + final String knownTargetId = "targetId"; + + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + final Long actionId = actions.get(0).getId(); + + // ensure specific action cannot be accessed via the actions resource + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "/" + actionId)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // not allowed methods + mvc.perform(post(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + mvc.perform(put(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + mvc.perform(delete(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + } + + private List generateTargetWithTwoUpdatesWithOneOverride(final String knownTargetId) { + return generateTargetWithTwoUpdatesWithOneOverrideWithMaintenanceWindow(knownTargetId, null, null, null); + } + + private List generateTargetWithTwoUpdatesWithOneOverrideWithMaintenanceWindow(final String knownTargetId, + final String schedule, final String duration, final String timezone) { + final Target target = testdataFactory.createTarget(knownTargetId); + + final Iterator sets = testdataFactory.createDistributionSets(2).iterator(); + final DistributionSet one = sets.next(); + final DistributionSet two = sets.next(); + + // Update + if (schedule == null) { + final List updatedTargets = assignDistributionSet(one, Collections.singletonList(target)) + .getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList()); + // 2nd update + // sleep 10ms to ensure that we can sort by reportedAt + Awaitility.await().atMost(Duration.ONE_HUNDRED_MILLISECONDS).atLeast(5, TimeUnit.MILLISECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> updatedTargets.stream().allMatch(t -> t.getLastModifiedAt() > 0L)); + assignDistributionSet(two, updatedTargets); + } else { + final List updatedTargets = assignDistributionSetWithMaintenanceWindow(one.getId(), + target.getControllerId(), schedule, duration, timezone).getAssignedEntity().stream() + .map(Action::getTarget).collect(Collectors.toList()); + // 2nd update + // sleep 10ms to ensure that we can sort by reportedAt + Awaitility.await().atMost(Duration.ONE_HUNDRED_MILLISECONDS).atLeast(5, TimeUnit.MILLISECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> updatedTargets.stream().allMatch(t -> t.getLastModifiedAt() > 0L)); + assignDistributionSetWithMaintenanceWindow(two.getId(), updatedTargets.get(0).getControllerId(), schedule, + duration, timezone); + } + + // two updates, one cancellation + final List actions = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE) + .getContent(); + + assertThat(actions).hasSize(2); + return actions; + } + + private static String generateActionLink(final String targetId, final Long actionId) { + return "http://localhost" + MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + targetId + "/" + + MgmtRestConstants.TARGET_V1_ACTIONS + "/" + actionId; + } + + private static String generateTargetLink(final String targetId) { + return "http://localhost" + MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + targetId; + } + + private static String generateDistributionSetLink(final Action action) { + return "http://localhost" + MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + + action.getDistributionSet().getId(); + } + +} diff --git a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/actions-api-guide.adoc b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/actions-api-guide.adoc new file mode 100644 index 000000000..a4ca2a594 --- /dev/null +++ b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/actions-api-guide.adoc @@ -0,0 +1,73 @@ +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: macro +:toclevels: 1 +:sectlinks: +:linkattrs: + +[[actions]] += Actions + +toc::[] + +== GET /rest/v1/actions + +=== Implementation notes + +Handles the GET request of retrieving all actions within Hawkbit. Required permission: READ_TARGET + +=== Get paged list of actions + +==== CURL + +include::{snippets}/actions/get-actions/curl-request.adoc[] + +==== Request URL + +A `GET` request is used to access the actions + +include::{snippets}/actions/get-actions/http-request.adoc[] + +==== Request query parameter + +include::{snippets}/actions/get-actions-with-parameters/request-parameters.adoc[] + +==== Request parameter example + +include::{snippets}/actions/get-actions-with-parameters/http-request.adoc[] + +=== Response (Status 200) + +==== Response fields + +include::{snippets}/actions/get-actions/response-fields.adoc[] + +==== Response example + +include::{snippets}/actions/get-actions/http-response.adoc[] + +==== Response example for representation mode 'full' + +include::{snippets}/actions/get-actions-with-parameters/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[] +|=== + +== Additional content + +[[error-body]] +=== Error body + +include::../errors/error-response-body.adoc[] diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java index a07ceb143..32390657a 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java @@ -33,6 +33,7 @@ public final class MgmtApiModelProperties { public static final String LINK_TO_OPTIONAL_SMT = "Link to optional software modules types in this distribution set type."; public static final String LINK_TO_ROLLOUT = "The link to the rollout."; public static final String LINK_TO_TARGET_TYPE = "The link to the target type."; + public static final String LINK_TO_TARGET = "The link to the target."; // software module types public static final String SMT_TYPE = "The type of the software module identified by its key."; @@ -158,6 +159,8 @@ public final class MgmtApiModelProperties { public static final String ACTION_EXECUTION_STATUS = "Status of action."; + public static final String ACTION_DETAIL_STATUS = "Detailed status of action."; + public static final String ACTION_LIST = "List of actions."; public static final String ACTION_WEIGHT = "Weight of the action showing the importance of the update."; @@ -245,4 +248,7 @@ public final class MgmtApiModelProperties { public static final String CONFIG_PARAM = "The name of the configuration parameter."; public static final String DS_NEW_ASSIGNED_ACTIONS = "The newly created actions as a result of this assignment"; + + public static final String REPRESENTATION_MODE = "The representation mode. Can be \"full\" or \"compact\". Defaults to \"compact\""; + } diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/ActionResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/ActionResourceDocumentationTest.java new file mode 100644 index 000000000..042475653 --- /dev/null +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/ActionResourceDocumentationTest.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2018 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.mgmt.documentation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; +import org.eclipse.hawkbit.repository.ActionStatusFields; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.rest.documentation.AbstractApiRestDocumentation; +import org.eclipse.hawkbit.rest.documentation.ApiModelPropertiesGeneric; +import org.eclipse.hawkbit.rest.documentation.MgmtApiModelProperties; +import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.restdocs.payload.JsonFieldType; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; + +/** + * Documentation generation for Management API for {@link Action}. + */ +@Feature("Spring Rest Docs Tests - Action") +@Story("Action Resource") +public class ActionResourceDocumentationTest extends AbstractApiRestDocumentation { + + private final String targetId = "target137"; + + @Override + public String getResourceName() { + return "actions"; + } + + @Test + @Description("Handles the GET request of retrieving all actions. Required Permission: READ_TARGET.") + public void getActions() throws Exception { + enableMultiAssignments(); + generateRolloutActionForTarget(targetId); + + mockMvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING)).andExpect(status().isOk()) + .andDo(MockMvcResultPrinter.print()) + .andDo(this.document.document(responseFields( + fieldWithPath("size").type(JsonFieldType.NUMBER).description(ApiModelPropertiesGeneric.SIZE), + fieldWithPath("total").description(ApiModelPropertiesGeneric.TOTAL_ELEMENTS), + fieldWithPath("content[]").description(MgmtApiModelProperties.ACTION_LIST), + fieldWithPath("content[].createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), + fieldWithPath("content[].createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), + fieldWithPath("content[].lastModifiedBy") + .description(ApiModelPropertiesGeneric.LAST_MODIFIED_BY).type("String"), + fieldWithPath("content[].lastModifiedAt") + .description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT).type("String"), + fieldWithPath("content[].type").description(MgmtApiModelProperties.ACTION_TYPE) + .attributes(key("value").value("['update', 'cancel']")), + + fieldWithPath("content[].status").description(MgmtApiModelProperties.ACTION_EXECUTION_STATUS) + .attributes(key("value").value("['finished', 'pending']")), + fieldWithPath("content[].detailStatus").description(MgmtApiModelProperties.ACTION_DETAIL_STATUS) + .attributes(key("value").value( + "['finished', 'error', 'running', 'warning', 'scheduled', 'canceling', 'canceled', 'download', 'downloaded', 'retrieved', 'cancel_rejected']")), + fieldWithPath("content[]._links").description(MgmtApiModelProperties.LINK_TO_ACTION), + fieldWithPath("content[].id").description(MgmtApiModelProperties.ACTION_ID), + fieldWithPath("content[].weight").description(MgmtApiModelProperties.ACTION_WEIGHT), + fieldWithPath("content[].rollout").description(MgmtApiModelProperties.ACTION_ROLLOUT), + fieldWithPath("content[].rolloutName") + .description(MgmtApiModelProperties.ACTION_ROLLOUT_NAME)))); + } + + @Test + @Description("Handles the GET request of retrieving all actions based on parameters. Required Permission: READ_TARGET.") + public void getActionsWithParameters() throws Exception { + final Action action = generateRolloutActionForTarget(targetId); + + mockMvc.perform( + get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "?limit=10&sort=id:ASC&offset=0&q=rollout.id==" + + action.getRollout().getId() + "&representation=full")) + .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andDo(this.document.document(requestParameters( + parameterWithName("limit").attributes(key("type").value("query")) + .description(ApiModelPropertiesGeneric.LIMIT), + parameterWithName("sort").description(ApiModelPropertiesGeneric.SORT), + parameterWithName("offset").description(ApiModelPropertiesGeneric.OFFSET), + parameterWithName("q").description(ApiModelPropertiesGeneric.FIQL), + parameterWithName("representation").description(MgmtApiModelProperties.REPRESENTATION_MODE)))); + } + + private Action generateRolloutActionForTarget(final String knownControllerId) throws Exception { + return generateActionForTarget(knownControllerId, true, false, null, null, null, true); + } + + private Action generateActionForTarget(final String knownControllerId, final boolean inSync, + final boolean timeforced, final String maintenanceWindowSchedule, final String maintenanceWindowDuration, + final String maintenanceWindowTimeZone, final boolean createRollout) throws Exception { + final PageRequest pageRequest = PageRequest.of(0, 1, Direction.ASC, ActionStatusFields.ID.getFieldName()); + + createTargetByGivenNameWithAttributes(knownControllerId, inSync, timeforced, createDistributionSet(), + maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone, createRollout); + + final List actions = deploymentManagement.findActionsAll(pageRequest).getContent(); + + assertThat(actions).hasSize(1); + return actions.get(0); + } + +} diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java index a4dedd75d..631fe6c4e 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java @@ -155,6 +155,8 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati .description(MgmtApiModelProperties.ROLLOUT_LINKS_APPROVE)); allFieldDescriptor.add( fieldWithPath(arrayPrefix + "_links.deny").description(MgmtApiModelProperties.ROLLOUT_LINKS_DENY)); + allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.distributionset") + .description(MgmtApiModelProperties.LINK_TO_DS)); } return new DocumenationResponseFieldsSnippet(allFieldDescriptor); diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java index eb82fa5af..c14c24217 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java @@ -237,6 +237,10 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio fieldWithPath("content[].status") .description(MgmtApiModelProperties.ACTION_EXECUTION_STATUS) .attributes(key("value").value("['finished', 'pending']")), + fieldWithPath("content[].detailStatus") + .description(MgmtApiModelProperties.ACTION_DETAIL_STATUS) + .attributes(key("value").value( + "['finished', 'error', 'running', 'warning', 'scheduled', 'canceling', 'canceled', 'download', 'downloaded', 'retrieved', 'cancel_rejected']")), fieldWithPath("content[]._links").description(MgmtApiModelProperties.LINK_TO_ACTION), fieldWithPath("content[].id").description(MgmtApiModelProperties.ACTION_ID), fieldWithPath("content[].weight").description(MgmtApiModelProperties.ACTION_WEIGHT), @@ -270,11 +274,15 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio .description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT).type("String"), fieldWithPath("content[].type").description(MgmtApiModelProperties.ACTION_TYPE) .attributes(key("value").value("['update', 'cancel']")), - fieldWithPath("content[].status") .description(MgmtApiModelProperties.ACTION_EXECUTION_STATUS) .attributes(key("value").value("['finished', 'pending']")), - fieldWithPath("content[]._links").description(MgmtApiModelProperties.LINK_TO_ACTION), + fieldWithPath("content[].detailStatus") + .description(MgmtApiModelProperties.ACTION_DETAIL_STATUS) + .attributes(key("value").value( + "['finished', 'error', 'running', 'warning', 'scheduled', 'canceling', 'canceled', 'download', 'downloaded', 'retrieved', 'cancel_rejected']")), + fieldWithPath("content[]._links.self") + .description(MgmtApiModelProperties.LINK_TO_ACTION), fieldWithPath("content[].id").description(MgmtApiModelProperties.ACTION_ID), fieldWithPath("content[].weight").description(MgmtApiModelProperties.ACTION_WEIGHT), fieldWithPath("content[].maintenanceWindow") @@ -363,13 +371,17 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio .type("String"), fieldWithPath("status").description(MgmtApiModelProperties.ACTION_EXECUTION_STATUS) .attributes(key("value").value("['finished', 'pending']")), + fieldWithPath("detailStatus").description(MgmtApiModelProperties.ACTION_DETAIL_STATUS) + .attributes(key("value").value( + "['finished', 'error', 'running', 'warning', 'scheduled', 'canceling', 'canceled', 'download', 'downloaded', 'retrieved', 'cancel_rejected']")), fieldWithPath("rollout").description(MgmtApiModelProperties.ACTION_ROLLOUT), fieldWithPath("rolloutName").description(MgmtApiModelProperties.ACTION_ROLLOUT_NAME), fieldWithPath("_links.self").ignored(), fieldWithPath("_links.distributionset").description(MgmtApiModelProperties.LINK_TO_DS), fieldWithPath("_links.status") .description(MgmtApiModelProperties.LINKS_ACTION_STATUSES), - fieldWithPath("_links.rollout").description(MgmtApiModelProperties.LINK_TO_ROLLOUT)))); + fieldWithPath("_links.rollout").description(MgmtApiModelProperties.LINK_TO_ROLLOUT), + fieldWithPath("_links.target").description(MgmtApiModelProperties.LINK_TO_TARGET)))); } @Test @@ -401,6 +413,9 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio .type("String"), fieldWithPath("status").description(MgmtApiModelProperties.ACTION_EXECUTION_STATUS) .attributes(key("value").value("['finished', 'pending']")), + fieldWithPath("detailStatus").description(MgmtApiModelProperties.ACTION_DETAIL_STATUS) + .attributes(key("value").value( + "['finished', 'error', 'running', 'warning', 'scheduled', 'canceling', 'canceled', 'download', 'downloaded', 'retrieved', 'cancel_rejected']")), fieldWithPath("maintenanceWindow") .description(MgmtApiModelProperties.MAINTENANCE_WINDOW), fieldWithPath("maintenanceWindow.schedule") @@ -414,7 +429,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio fieldWithPath("_links.self").ignored(), fieldWithPath("_links.distributionset").description(MgmtApiModelProperties.LINK_TO_DS), fieldWithPath("_links.status") - .description(MgmtApiModelProperties.LINKS_ACTION_STATUSES)))); + .description(MgmtApiModelProperties.LINKS_ACTION_STATUSES), + fieldWithPath("_links.target").description(MgmtApiModelProperties.LINK_TO_TARGET)))); } @Test @@ -451,10 +467,14 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio .attributes(key("value").value("['forced', 'soft', 'timeforced']")), fieldWithPath("status").description(MgmtApiModelProperties.ACTION_EXECUTION_STATUS) .attributes(key("value").value("['finished', 'pending']")), + fieldWithPath("detailStatus").description(MgmtApiModelProperties.ACTION_DETAIL_STATUS) + .attributes(key("value").value( + "['finished', 'error', 'running', 'warning', 'scheduled', 'canceling', 'canceled', 'download', 'downloaded', 'retrieved', 'cancel_rejected']")), fieldWithPath("_links.self").ignored(), fieldWithPath("_links.distributionset").description(MgmtApiModelProperties.LINK_TO_DS), fieldWithPath("_links.status") - .description(MgmtApiModelProperties.LINKS_ACTION_STATUSES)))); + .description(MgmtApiModelProperties.LINKS_ACTION_STATUSES), + fieldWithPath("_links.target").description(MgmtApiModelProperties.LINK_TO_TARGET)))); } @Test