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 index 8ebcd4137..c8edc7636 100644 --- 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 @@ -14,6 +14,7 @@ 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.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -54,4 +55,14 @@ public interface MgmtActionRestApi { @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); + /** + * Handles the GET request of retrieving a specific Action by actionId + * + * @param actionId + * + * @return the action + */ + @GetMapping(value = "/{actionId}", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity getAction( + @PathVariable("actionId") Long actionId); } \ No newline at end of file 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 index 7a5326dc1..55390ac55 100644 --- 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 @@ -14,6 +14,7 @@ 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.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.model.Action; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,16 +56,29 @@ public class MgmtActionResource implements MgmtActionRestApi { 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; - }); + final MgmtRepresentationMode repMode = getRepresentationModeFromString(representationModeParam); return ResponseEntity .ok(new PagedList<>(MgmtActionMapper.toResponse(actions.getContent(), repMode), totalActionCount)); } + @Override + public ResponseEntity getAction(final Long actionId) { + + final Action action = deploymentManagement.findAction(actionId) + .orElseThrow(() -> new EntityNotFoundException(Action.class, actionId)); + + return ResponseEntity.ok(MgmtActionMapper.toResponse(action, MgmtRepresentationMode.FULL)); + } + + private MgmtRepresentationMode getRepresentationModeFromString(final String representationModeParam) { + return 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; + }); + } + } 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 index bb2aec6b8..0641b14cf 100644 --- 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 @@ -54,10 +54,14 @@ class MgmtActionResourceTest extends AbstractManagementApiIntegrationTest { private static final String JSON_PATH_FIELD_SIZE = ".size"; private static final String JSON_PATH_FIELD_TOTAL = ".total"; + private static final String JSON_PATH_FIELD_ID = ".id"; + 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; + private static final String JSON_PATH_ACTION_ID = JSON_PATH_ROOT + JSON_PATH_FIELD_ID; + @Test @Description("Verifies that actions can be filtered based on action status.") void filterActionsByStatus() throws Exception { @@ -381,10 +385,6 @@ class MgmtActionResourceTest extends AbstractManagementApiIntegrationTest { 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()); @@ -394,6 +394,28 @@ class MgmtActionResourceTest extends AbstractManagementApiIntegrationTest { .andExpect(status().isMethodNotAllowed()); } + @Test + @Description("Verifies that the correct action is returned") + void shouldRetrieveCorrectActionById() throws Exception { + final String knownTargetId = "targetId"; + + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + final Long actionId = actions.get(0).getId(); + + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "/" + actionId)) + .andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath(JSON_PATH_ACTION_ID, equalTo(actionId.intValue()))); + } + + @Test + @Description("Verifies that NOT_FOUND is returned when there is no such action.") + void requestActionThatDoesNotExistsLeadsToNotFound() throws Exception { + mvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "/" + 101)) + .andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + } + private List generateTargetWithTwoUpdatesWithOneOverride(final String knownTargetId) { return generateTargetWithTwoUpdatesWithOneOverrideWithMaintenanceWindow(knownTargetId, null, null, null); } 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 index a4ca2a594..1c19c5a5c 100644 --- 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 @@ -65,6 +65,44 @@ include::../errors/406.adoc[] include::../errors/429.adoc[] |=== +== GET /rest/v1/actions/{actionId} + +=== Implementation notes + +Handles the GET request of retrieving a single action within Hawkbit by actionId. + +=== Get single action + +==== CURL + +include::{snippets}/actions/get-action/curl-request.adoc[] + +==== Request URL + +A `GET` request is used to access a single action + +include::{snippets}/actions/get-action/http-request.adoc[] + +=== Response (Status 200) + +==== Response fields + +include::{snippets}/actions/get-action/response-fields.adoc[] + +==== Response example + +include::{snippets}/actions/get-action/http-response.adoc[] + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/404.adoc[] +include::../errors/406.adoc[] +include::../errors/429.adoc[] +|=== + == Additional content [[error-body]] 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 index 512804fe6..b8fca2d8a 100644 --- 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 @@ -11,8 +11,7 @@ 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.request.RequestDocumentation.*; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -103,6 +102,47 @@ public class ActionResourceDocumentationTest extends AbstractApiRestDocumentatio parameterWithName("representation").description(MgmtApiModelProperties.REPRESENTATION_MODE)))); } + @Test + @Description("Handles the GET request of retrieving a specific action.") + public void getAction() throws Exception { + final Action action = generateRolloutActionForTarget(targetId); + provideCodeFeedback(action, 200); + + assertThat(deploymentManagement.findAction(action.getId()).get().getActionType()) + .isEqualTo(Action.ActionType.FORCED); + + mockMvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING + "/{actionId}", action.getId())) + .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andDo(this.document.document( + pathParameters(parameterWithName("actionId").description(ApiModelPropertiesGeneric.ITEM_ID)), + responseFields(fieldWithPath("createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), + fieldWithPath("createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), + fieldWithPath("id").description(MgmtApiModelProperties.ACTION_ID), + fieldWithPath("lastModifiedBy").description(ApiModelPropertiesGeneric.LAST_MODIFIED_BY) + .type("String"), + fieldWithPath("lastModifiedAt").description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT) + .type("String"), + fieldWithPath("type").description(MgmtApiModelProperties.ACTION_TYPE) + .attributes(key("value").value("['update', 'cancel']")), + fieldWithPath("forceType").description(MgmtApiModelProperties.ACTION_FORCE_TYPE) + .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']")), + optionalRequestFieldWithPath("lastStatusCode") + .description(MgmtApiModelProperties.ACTION_LAST_STATUS_CODE).type("Integer"), + 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.target").description(MgmtApiModelProperties.LINK_TO_TARGET)))); + } + private Action generateRolloutActionForTarget(final String knownControllerId) throws Exception { return generateActionForTarget(knownControllerId, true, false, null, null, null, true); }