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 271f2113b..a86a7ef4b 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 @@ -75,6 +75,9 @@ public class MgmtAction extends MgmtBaseEntity { @JsonProperty private String rolloutName; + @JsonProperty + private Integer lastStatusCode; + public MgmtMaintenanceWindow getMaintenanceWindow() { return maintenanceWindow; } @@ -155,4 +158,12 @@ public class MgmtAction extends MgmtBaseEntity { this.detailStatus = detailStatus; } + public Integer getLastStatusCode() { + return lastStatusCode; + } + + public void setLastStatusCode(final Integer lastStatusCode) { + this.lastStatusCode = lastStatusCode; + } + } 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 def5ca4ea..6d3094c37 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 @@ -40,8 +40,8 @@ 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.AutoConfirmationStatus; +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; @@ -257,6 +257,10 @@ public final class MgmtTargetMapper { result.setDetailStatus(action.getStatus().toString().toLowerCase()); + action.getLastActionStatusCode().ifPresent(statusCode -> { + result.setLastStatusCode(statusCode); + }); + final Rollout rollout = action.getRollout(); if (rollout != null) { result.setRollout(rollout.getId()); diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java index 11e948d9f..4ae815cd2 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java @@ -50,6 +50,7 @@ import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.ActionFields; import org.eclipse.hawkbit.repository.Identifiable; +import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.Action; @@ -141,8 +142,8 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { final int limitSize = 2; final String knownTargetId = "targetId"; final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); - controllerManagement.addUpdateActionStatus( - entityFactory.actionStatus().create(actions.get(0).getId()).status(Status.FINISHED).message("test")); + assertThat(actions).hasSize(2); + updateActionStatus(actions.get(0), Status.FINISHED, null, "test"); final PageRequest pageRequest = PageRequest.of(0, 1000, Direction.ASC, ActionFields.ID.getFieldName()); final Action action = deploymentManagement.findActionsByTarget(knownTargetId, pageRequest).getContent().get(0); @@ -1353,7 +1354,7 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { if (confirmationFlowActive) { enableConfirmationFlow(); } - + final JSONObject jsonPayload = new JSONObject(); jsonPayload.put("id", set.getId()); if (confirmationRequired != null) { @@ -2050,9 +2051,9 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { final JSONObject bodyValid = getAssignmentObject(dsId, MgmtActionType.FORCED, 98); mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(bodyValid.toString()) - .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.multiassignmentNotEnabled"))); + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.multiassignmentNotEnabled"))); } @Test @@ -2207,7 +2208,7 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { @Description("Ensures that a post request for creating target with target type works.") void createTargetWithExistingTargetType() throws Exception { // create target type - List targetTypes = testdataFactory.createTargetTypes("targettype", 1); + final List targetTypes = testdataFactory.createTargetTypes("targettype", 1); assertThat(targetTypes).hasSize(1); final Target target = entityFactory.target().create().controllerId("targetcontroller").name("testtarget") @@ -2229,11 +2230,11 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { @Description("Ensures that a put request for updating targets with target type works.") void updateTargetTypeInTarget() throws Exception { // create target type - List targetTypes = testdataFactory.createTargetTypes("targettype", 2); + final List targetTypes = testdataFactory.createTargetTypes("targettype", 2); assertThat(targetTypes).hasSize(2); - String controllerId = "targetcontroller"; - Target target = testdataFactory.createTarget(controllerId, "testtarget", targetTypes.get(0).getId()); + final String controllerId = "targetcontroller"; + final Target target = testdataFactory.createTarget(controllerId, "testtarget", targetTypes.get(0).getId()); assertThat(target).isNotNull(); assertThat(target.getTargetType().getId()).isEqualTo(targetTypes.get(0).getId()); @@ -2250,13 +2251,14 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { @Test @Description("Ensures that a post request for creating targets with unknown target type fails.") void addingNonExistingTargetTypeInTargetShouldFail() throws Exception { - long unknownTargetTypeId = 999; - String errorMsg = String.format("TargetType with given identifier {%s} does not exist.", unknownTargetTypeId); + final long unknownTargetTypeId = 999; + final String errorMsg = String.format("TargetType with given identifier {%s} does not exist.", + unknownTargetTypeId); - Optional targetType = targetTypeManagement.get(unknownTargetTypeId); + final Optional targetType = targetTypeManagement.get(unknownTargetTypeId); assertThat(targetType).isNotPresent(); - String controllerId = "targetcontroller"; + final String controllerId = "targetcontroller"; final Target target = entityFactory.target().create().controllerId(controllerId).name("testtarget").build(); final String targetList = JsonBuilder.targets(Collections.singletonList(target), false, unknownTargetTypeId); @@ -2271,12 +2273,12 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { @Description("Ensures that a post request for assign target type to target works.") void assignTargetTypeToTarget() throws Exception { // create target type - TargetType targetType = testdataFactory.findOrCreateTargetType("targettype"); + final TargetType targetType = testdataFactory.findOrCreateTargetType("targettype"); assertThat(targetType).isNotNull(); // create target - String targetControllerId = "targetcontroller"; - Target target = testdataFactory.createTarget(targetControllerId, "testtarget"); + final String targetControllerId = "targetcontroller"; + final Target target = testdataFactory.createTarget(targetControllerId, "testtarget"); assertThat(target).isNotNull(); // assign target type over rest resource @@ -2292,11 +2294,11 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { @Description("Ensures that a post request for assign a invalid target type to target fails.") void assignInvalidTargetTypeToTargetFails() throws Exception { // Invalid target type ID - long invalidTargetTypeId = 999; + final long invalidTargetTypeId = 999; // create target - String targetControllerId = "targetcontroller"; - Target target = testdataFactory.createTarget(targetControllerId, "testtarget"); + final String targetControllerId = "targetcontroller"; + final Target target = testdataFactory.createTarget(targetControllerId, "testtarget"); assertThat(target).isNotNull(); // assign invalid target type over rest resource @@ -2304,7 +2306,8 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { .content("{\"id\":" + invalidTargetTypeId + "}").contentType(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); - // verify response json exception message if body does not include id field + // verify response json exception message if body does not include id + // field final MvcResult mvcResult = mvc .perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + targetControllerId + "/targettype") .content("{\"unknownfield\":" + invalidTargetTypeId + "}") @@ -2321,11 +2324,12 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { @Description("Ensures that a delete request for unassign target type from target works.") void unassignTargetTypeFromTarget() throws Exception { // create target type - List targetTypes = testdataFactory.createTargetTypes("targettype", 1); + final List targetTypes = testdataFactory.createTargetTypes("targettype", 1); assertThat(targetTypes).hasSize(1); - String targetControllerId = "targetcontroller"; - Target target = testdataFactory.createTarget(targetControllerId, "testtarget", targetTypes.get(0).getId()); + final String targetControllerId = "targetcontroller"; + final Target target = testdataFactory.createTarget(targetControllerId, "testtarget", + targetTypes.get(0).getId()); assertThat(target).isNotNull(); assertThat(target.getTargetType().getId()).isEqualTo(targetTypes.get(0).getId()); @@ -2384,16 +2388,15 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { // GET with all possible responses mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + TARGET_V1_AUTO_CONFIRM, - knownTargetId)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) - .andExpect(jsonPath("active", equalTo(Boolean.TRUE))) - .andExpect(initiator == null ? jsonPath("initiator").doesNotExist() - : jsonPath("initiator", equalTo(initiator))) - .andExpect(remark == null ? jsonPath("remark").doesNotExist() : jsonPath("remark", equalTo(remark))) - .andExpect(jsonPath("_links.deactivate").exists()) - .andExpect(jsonPath("_links.activate").doesNotExist()); + knownTargetId)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("active", equalTo(Boolean.TRUE))) + .andExpect(initiator == null ? jsonPath("initiator").doesNotExist() + : jsonPath("initiator", equalTo(initiator))) + .andExpect(remark == null ? jsonPath("remark").doesNotExist() : jsonPath("remark", equalTo(remark))) + .andExpect(jsonPath("_links.deactivate").exists()) + .andExpect(jsonPath("_links.activate").doesNotExist()); } - @Test void getAutoConfirmStateFromTargetsEndpoint() throws Exception { final String knownTargetId = "targetId"; @@ -2453,6 +2456,64 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest { .andExpect(jsonPath("autoConfirmActive").exists()).andExpect(jsonPath("_links.autoConfirm").exists()); } + @Test + @Description("Verifies that the status code that was reported in the last action status update is correctly exposed via the action.") + void lastActionStatusCode() throws Exception { + + // prepare test + final DistributionSet dsA = testdataFactory.createDistributionSet(""); + final Target target = testdataFactory.createTarget("knownTargetId"); + final Action action = getFirstAssignedAction(assignDistributionSet(dsA, Collections.singletonList(target))); + + // no status update yet -> no status code + mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions/{actionId}", + target.getControllerId(), action.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("lastStatusCode").doesNotExist()); + + // update action status with status code + updateActionStatus(action, Status.RUNNING, 100); + mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions/{actionId}", + target.getControllerId(), action.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("lastStatusCode", equalTo(100))) + .andExpect(jsonPath("detailStatus", equalTo("running"))); + + // update action status without a status code + updateActionStatus(action, Status.RUNNING, null); + mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions/{actionId}", + target.getControllerId(), action.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("lastStatusCode").doesNotExist()) + .andExpect(jsonPath("detailStatus", equalTo("running"))); + + // update action status with status code + updateActionStatus(action, Status.ERROR, 432); + mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions/{actionId}", + target.getControllerId(), action.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("lastStatusCode", equalTo(432))) + .andExpect(jsonPath("detailStatus", equalTo("error"))); + } + + private Action updateActionStatus(final Action action, final Status status, final Integer statusCode) { + return updateActionStatus(action, status, statusCode, null); + } + + private Action updateActionStatus(final Action action, final Status status, final Integer statusCode, + final String message) { + + assertThat(action).isNotNull(); + assertThat(status).isNotNull(); + + final ActionStatusCreate actionStatus = entityFactory.actionStatus().create(action.getId()); + actionStatus.status(status); + if (statusCode != null) { + actionStatus.code(statusCode); + } + if (message != null) { + actionStatus.message(message); + } + + return controllerManagement.addUpdateActionStatus(actionStatus); + } + private static Stream possibleActiveStates() { return Stream.of(Arguments.of("someInitiator", "someRemark"), Arguments.of(null, "someRemark"), Arguments.of("someInitiator", null), Arguments.of(null, null)); 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 31373286e..bbe0d1184 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 @@ -162,6 +162,8 @@ public final class MgmtApiModelProperties { public static final String ACTION_STATUS_CODE = "(Optional) Code provided by the device related to the status."; + public static final String ACTION_LAST_STATUS_CODE = "(Optional) Code provided as part of the last status update that was sent by the device."; + public static final String ACTION_STATUS_LIST = "List of action status."; public static final String ACTION_EXECUTION_STATUS = "Status of action."; 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 042475653..512804fe6 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 @@ -52,7 +52,8 @@ public class ActionResourceDocumentationTest extends AbstractApiRestDocumentatio @Description("Handles the GET request of retrieving all actions. Required Permission: READ_TARGET.") public void getActions() throws Exception { enableMultiAssignments(); - generateRolloutActionForTarget(targetId); + final Action action = generateRolloutActionForTarget(targetId); + provideCodeFeedback(action, 200); mockMvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING)).andExpect(status().isOk()) .andDo(MockMvcResultPrinter.print()) @@ -74,6 +75,8 @@ public class ActionResourceDocumentationTest extends AbstractApiRestDocumentatio fieldWithPath("content[].detailStatus").description(MgmtApiModelProperties.ACTION_DETAIL_STATUS) .attributes(key("value").value( "['finished', 'error', 'running', 'warning', 'scheduled', 'canceling', 'canceled', 'download', 'downloaded', 'retrieved', 'cancel_rejected']")), + optionalRequestFieldWithPath("content[].lastStatusCode") + .description(MgmtApiModelProperties.ACTION_LAST_STATUS_CODE).type("Integer"), fieldWithPath("content[]._links").description(MgmtApiModelProperties.LINK_TO_ACTION), fieldWithPath("content[].id").description(MgmtApiModelProperties.ACTION_ID), fieldWithPath("content[].weight").description(MgmtApiModelProperties.ACTION_WEIGHT), 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 069d1859a..b006d81ec 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 @@ -107,8 +107,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio fieldWithPath("content[].autoConfirmActive") .description(MgmtApiModelProperties.AUTO_CONFIRM_ACTIVE), fieldWithPath("content[].installedAt").description(MgmtApiModelProperties.INSTALLED_AT), - fieldWithPath("content[].lastModifiedAt").description( - ApiModelPropertiesGeneric.LAST_MODIFIED_AT).type("Number"), + fieldWithPath("content[].lastModifiedAt") + .description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT).type("Number"), fieldWithPath("content[].lastModifiedBy") .description(ApiModelPropertiesGeneric.LAST_MODIFIED_BY).type("String"), fieldWithPath("content[].ipAddress").description(MgmtApiModelProperties.IP_ADDRESS) @@ -362,6 +362,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio public void getActionFromTarget() throws Exception { enableMultiAssignments(); final Action action = generateRolloutActionForTarget(targetId, true, true); + provideCodeFeedback(action, 200); + assertThat(deploymentManagement.findAction(action.getId()).get().getActionType()) .isEqualTo(ActionType.TIMEFORCED); @@ -390,6 +392,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio 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(), @@ -406,6 +410,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio enableMultiAssignments(); final Action action = generateActionForTarget(targetId, true, true, getTestSchedule(2), getTestDuration(1), getTestTimeZone()); + provideCodeFeedback(action, 200); mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + MgmtRestConstants.TARGET_V1_ACTIONS + "/{actionId}", targetId, action.getId())) @@ -432,6 +437,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio 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("maintenanceWindow") .description(MgmtApiModelProperties.MAINTENANCE_WINDOW), fieldWithPath("maintenanceWindow.schedule") @@ -1005,11 +1012,6 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio return generateActionForTarget(knownControllerId, inSync, timeforced, null, null, null, true); } - private Action generateActionForTarget(final String knownControllerId, final boolean inSync, - final boolean timeforced) throws Exception { - return generateActionForTarget(knownControllerId, inSync, timeforced, null, null, null); - } - private Action generateActionForTarget(final String knownControllerId, final boolean inSync, final boolean timeforced, final String maintenanceWindowSchedule, final String maintenanceWindowDuration, final String maintenanceWindowTimeZone) throws Exception {