diff --git a/docs/src/main/resources/documentation/interfaces/ddi-api.md b/docs/src/main/resources/documentation/interfaces/ddi-api.md index 132a60cdf..918651959 100644 --- a/docs/src/main/resources/documentation/interfaces/ddi-api.md +++ b/docs/src/main/resources/documentation/interfaces/ddi-api.md @@ -97,7 +97,14 @@ _Example Response_ } ] }, - "id": "1" + "id": "1", + "actionHistory": { + "status": "RETRIEVED", + "messages": [ + "Installing update", + "Downloading artifacts" + ] + } } ``` diff --git a/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiActionHistory.java b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiActionHistory.java new file mode 100644 index 000000000..8bc8ce6d6 --- /dev/null +++ b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiActionHistory.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) Siemens AG, 2017 + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ddi.json.model; + +import java.util.List; + +import org.eclipse.hawkbit.ddi.rest.api.DdiRootControllerRestApi; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +/** + * Provide action history information to the controller as part of response to + * {@link DdiRootControllerRestApi#getControllerBasedeploymentAction}: 1. + * Current action status at the server; 2. List of messages from action history + * that were sent to server earlier by the controller using + * {@link DdiActionFeedback}. + */ + +@JsonPropertyOrder({ "status", "messages" }) +public class DdiActionHistory { + + @JsonProperty("status") + private final String actionStatus; + + @JsonProperty("messages") + private final List messages; + + /** + * Parameterized constructor for creating {@link DdiActionHistory}. + * + * @param actionStatus + * is the current action status at the server + * @param messages + * is a list of messages retrieved from action history. + */ + @JsonCreator + public DdiActionHistory(@JsonProperty("status") final String actionStatus, + @JsonProperty("messages") List messages) { + this.actionStatus = actionStatus; + this.messages = messages; + } + + @Override + public String toString() { + return "Action history [" + "status=" + actionStatus + ", messages={" + messages.toString() + "}]"; + } +} diff --git a/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiDeploymentBase.java b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiDeploymentBase.java index a8f5ee72b..c5a1630b4 100644 --- a/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiDeploymentBase.java +++ b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiDeploymentBase.java @@ -13,42 +13,70 @@ import javax.validation.constraints.NotNull; import org.springframework.hateoas.ResourceSupport; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; /** * Update action resource. */ +@JsonPropertyOrder({ "id", "deployment", "actionHistory" }) public class DdiDeploymentBase extends ResourceSupport { @JsonProperty("id") @NotNull private final String deplyomentId; + @JsonProperty("deployment") @NotNull private final DdiDeployment deployment; + /** + * Action history containing current action status and a list of feedback + * messages received earlier from the controller. + */ + @JsonProperty("actionHistory") + @JsonInclude(JsonInclude.Include.NON_NULL) + private final DdiActionHistory actionHistory; + /** * Constructor. * * @param id * of the update action * @param deployment - * details. + * details + * @param actionHistory + * containing current action status and a list of feedback + * messages received earlier from the controller. */ @JsonCreator public DdiDeploymentBase(@JsonProperty("id") final String id, - @JsonProperty("deplyomentId") final DdiDeployment deployment) { + @JsonProperty("deplyomentId") final DdiDeployment deployment, + @JsonProperty("actionHistory") final DdiActionHistory actionHistory) { this.deplyomentId = id; this.deployment = deployment; + this.actionHistory = actionHistory; } public DdiDeployment getDeployment() { return deployment; } + /** + * Returns the action history containing current action status and a list of + * feedback messages received earlier from the controller. + * + * @return {@link DdiActionHistory} + */ + public DdiActionHistory getActionHistory() { + return actionHistory; + } + @Override public String toString() { - return "DeploymentBase [id=" + deplyomentId + ", deployment=" + deployment + "]"; + return "DeploymentBase [id=" + deplyomentId + ", deployment=" + deployment + " actionHistory=" + + actionHistory.toString() + "]"; } } diff --git a/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiStatus.java b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiStatus.java index 78deeefe8..64d360b9d 100644 --- a/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiStatus.java +++ b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiStatus.java @@ -13,6 +13,7 @@ import java.util.List; import javax.validation.Valid; import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -31,6 +32,7 @@ public class DdiStatus { @Valid private final DdiResult result; + @Size(min = 0, max = 50) private final List details; /** diff --git a/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java index 25be5b1f8..bacb70ee4 100644 --- a/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java +++ b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java @@ -43,6 +43,13 @@ public final class DdiRestConstants { */ public static final String CONFIG_DATA_ACTION = "configData"; + /** + * Default value specifying that no action history to be sent as part of + * response to deploymentBase + * {@link DdiRootControllerRestApi#getControllerBasedeploymentAction}. + */ + public static final String NO_ACTION_HISTORY = "0"; + private DdiRestConstants() { // constant class, private constructor. } diff --git a/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java index ef434914d..90607a426 100644 --- a/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java +++ b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java @@ -136,6 +136,17 @@ public interface DdiRootControllerRestApi { * an hashcode of the resource which indicates if the action has * been changed, e.g. from 'soft' to 'force' and the eTag needs * to be re-generated + * @param actionHistoryMessageCount + * specifies the number of messages to be returned from action + * history. Regardless of the passed value, in order to restrict + * resource utilization by controllers, maximum number of + * messages that are retrieved from database is limited by + * {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}. + * actionHistoryMessageCount < 0, retrieves the maximum allowed + * number of action status messages from history; + * actionHistoryMessageCount = 0, does not retrieve any message; + * and actionHistoryMessageCount > 0, retrieves the specified + * number of messages, limited by maximum allowed number. * @param request * the HTTP request injected by spring * @return the response @@ -146,7 +157,8 @@ public interface DdiRootControllerRestApi { ResponseEntity getControllerBasedeploymentAction(@PathVariable("tenant") final String tenant, @PathVariable("controllerId") @NotEmpty final String controllerId, @PathVariable("actionId") @NotEmpty final Long actionId, - @RequestParam(value = "c", required = false, defaultValue = "-1") final int resource); + @RequestParam(value = "c", required = false, defaultValue = "-1") final int resource, + @RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount); /** * This is the feedback channel for the {@link DdiDeploymentBase} action. diff --git a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java index 4b5f664c3..a04ba535a 100644 --- a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java +++ b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java @@ -118,7 +118,7 @@ public final class DataConversionHelper { result.add(ControllerLinkBuilder .linkTo(ControllerLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant()) .getControllerBasedeploymentAction(tenantAware.getCurrentTenant(), - target.getControllerId(), action.getId(), calculateEtag(action))) + target.getControllerId(), action.getId(), calculateEtag(action), null)) .withRel(DdiRestConstants.DEPLOYMENT_BASE_ACTION)); } } diff --git a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java index 63a2896a6..4ff0eae9a 100644 --- a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java +++ b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java @@ -19,6 +19,7 @@ import javax.validation.Valid; import org.eclipse.hawkbit.api.ArtifactUrlHandler; import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback; +import org.eclipse.hawkbit.ddi.json.model.DdiActionHistory; import org.eclipse.hawkbit.ddi.json.model.DdiCancel; import org.eclipse.hawkbit.ddi.json.model.DdiCancelActionToStop; import org.eclipse.hawkbit.ddi.json.model.DdiChunk; @@ -28,6 +29,7 @@ import org.eclipse.hawkbit.ddi.json.model.DdiDeployment; import org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType; import org.eclipse.hawkbit.ddi.json.model.DdiDeploymentBase; import org.eclipse.hawkbit.ddi.json.model.DdiResult.FinalResult; +import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants; import org.eclipse.hawkbit.ddi.rest.api.DdiRootControllerRestApi; import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.ControllerManagement; @@ -231,7 +233,8 @@ public class DdiRootController implements DdiRootControllerRestApi { public ResponseEntity getControllerBasedeploymentAction( @PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId, @PathVariable("actionId") final Long actionId, - @RequestParam(value = "c", required = false, defaultValue = "-1") final int resource) { + @RequestParam(value = "c", required = false, defaultValue = "-1") final int resource, + @RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount) { LOG.debug("getControllerBasedeploymentAction({},{})", controllerId, resource); final Target target = controllerManagement.findByControllerId(controllerId) @@ -251,8 +254,15 @@ public class DdiRootController implements DdiRootControllerRestApi { final HandlingType handlingType = action.isForce() ? HandlingType.FORCED : HandlingType.ATTEMPT; + List actionHistoryMsgs = controllerManagement.getActionHistoryMessages(action.getId(), + actionHistoryMessageCount == null ? Integer.parseInt(DdiRestConstants.NO_ACTION_HISTORY) + : actionHistoryMessageCount); + + DdiActionHistory actionHistory = actionHistoryMsgs.isEmpty() ? null + : new DdiActionHistory(action.getStatus().name(), actionHistoryMsgs); + final DdiDeploymentBase base = new DdiDeploymentBase(Long.toString(action.getId()), - new DdiDeployment(handlingType, handlingType, chunks)); + new DdiDeployment(handlingType, handlingType, chunks), actionHistory); LOG.debug("Found an active UpdateAction for target {}. returning deyploment: {}", controllerId, base); diff --git a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java index 0e742e380..10fb35e3a 100644 --- a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java +++ b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java @@ -14,6 +14,8 @@ import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpre import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION; import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.SYSTEM_ROLE; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.startsWith; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -60,6 +62,9 @@ import ru.yandex.qatools.allure.annotations.Stories; @Stories("Root Poll Resource") public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { + private static final String TARGET_COMPLETED_INSTALLATION_MSG = "Target completed installation."; + private static final String TARGET_PROCEEDING_INSTALLATION_MSG = "Target proceeding installation."; + private static final String TARGET_SCHEDULED_INSTALLATION_MSG = "Target scheduled installation."; @Autowired private HawkbitSecurityProperties securityProperties; @@ -352,4 +357,142 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { .andDo(MockMvcResultPrinter.print()).andExpect(status().isGone()); } + @Test + @Description("Test to verify that only a specific count of messages are returned based on the input actionHistory for getControllerDeploymentActionFeedback endpoint.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void testActionHistoryCount() throws Exception { + final DistributionSet ds = testdataFactory.createDistributionSet(""); + Target savedTarget = testdataFactory.createTarget("911"); + savedTarget = assignDistributionSet(ds.getId(), savedTarget.getControllerId()).getAssignedEntity().iterator() + .next(); + final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) + .getContent().get(0); + + mvc.perform(post("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "scheduled", + TARGET_SCHEDULED_INSTALLATION_MSG)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(post("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "proceeding", + TARGET_PROCEEDING_INSTALLATION_MSG)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(post("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "closed", + "success", TARGET_COMPLETED_INSTALLATION_MSG)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(get("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "?actionHistory=3", + tenantAware.getCurrentTenant()).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.actionHistory.messages", + hasItem(containsString(TARGET_PROCEEDING_INSTALLATION_MSG)))) + .andExpect(jsonPath("$.actionHistory.messages", + hasItem(containsString(TARGET_SCHEDULED_INSTALLATION_MSG)))); + } + + @Test + @Description("Test to verify that a zero input value of actionHistory results in no action history appended for getControllerDeploymentActionFeedback endpoint.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void testActionHistoryZeroInput() throws Exception { + final DistributionSet ds = testdataFactory.createDistributionSet(""); + Target savedTarget = testdataFactory.createTarget("911"); + savedTarget = assignDistributionSet(ds.getId(), savedTarget.getControllerId()).getAssignedEntity().iterator() + .next(); + final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) + .getContent().get(0); + + mvc.perform(post("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "scheduled", + TARGET_SCHEDULED_INSTALLATION_MSG)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(post("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "proceeding", + TARGET_PROCEEDING_INSTALLATION_MSG)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(post("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "closed", + "success", TARGET_COMPLETED_INSTALLATION_MSG)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(get("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "?actionHistory=-2", + tenantAware.getCurrentTenant()).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + } + + @Test + @Description("Test to verify that entire action history is returned if the input value for actionHistory is -1, for getControllerDeploymentActionFeedback endpoint.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void testActionHistoryNegativeInput() throws Exception { + final DistributionSet ds = testdataFactory.createDistributionSet(""); + Target savedTarget = testdataFactory.createTarget("911"); + savedTarget = assignDistributionSet(ds.getId(), savedTarget.getControllerId()).getAssignedEntity().iterator() + .next(); + final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) + .getContent().get(0); + + mvc.perform(post("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "scheduled", + TARGET_SCHEDULED_INSTALLATION_MSG)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(post("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "proceeding", + TARGET_PROCEEDING_INSTALLATION_MSG)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(post("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "closed", + "success", TARGET_COMPLETED_INSTALLATION_MSG)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(get("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "?actionHistory=-1", + tenantAware.getCurrentTenant()).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.actionHistory.messages", + hasItem(containsString(TARGET_SCHEDULED_INSTALLATION_MSG)))) + .andExpect(jsonPath("$.actionHistory.messages", + hasItem(containsString(TARGET_PROCEEDING_INSTALLATION_MSG)))) + .andExpect(jsonPath("$.actionHistory.messages", + hasItem(containsString(TARGET_COMPLETED_INSTALLATION_MSG)))); + } } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java index 3211eee8a..7c29b4e19 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.repository; import java.net.URI; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -317,4 +318,30 @@ public interface ControllerManagement { + SpringEvalExpressions.IS_SYSTEM_CODE) Optional findByTargetId(@NotNull Long targetId); + /** + * Retrieves the specified number of messages from action history of the + * given {@link Action} based on messageCount. Regardless of the value of + * messageCount, in order to restrict resource utilization by controllers, + * maximum number of messages that are retrieved from database is limited by + * {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}. messageCount < + * 0, retrieves the maximum allowed number of action status messages from + * history; messageCount = 0, does not retrieve any message; and + * messageCount > 0, retrieves the specified number of messages, limited by + * maximum allowed number. A controller sends the feedback for an + * {@link ActionStatus} as a list of messages; while returning the messages, + * even though the messages from multiple {@link ActionStatus} are retrieved + * in descending order by the reported time + * ({@link ActionStatus#getOccurredAt()}), i.e. latest ActionStatus first, + * the sub-ordering of messages from within single {@link ActionStatus} is + * unspecified. + * + * @param actionId + * to be filtered on + * @param messageCount + * is the number of messages to return from history + * + * @return action history. + */ + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + List getActionHistoryMessages(@NotNull Long actionId, final int messageCount); } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryConstants.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryConstants.java index f2cd3f9f6..6a099cd48 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryConstants.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryConstants.java @@ -30,6 +30,12 @@ public final class RepositoryConstants { */ public static final int DEFAULT_DS_TYPES_IN_TENANT = 2; + /** + * Maximum number of messages that can be retrieved by a controller for an + * {@link Action}. + */ + public static final int MAX_ACTION_HISTORY_MSG_COUNT = 100; + private RepositoryConstants() { // Utility class. } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionStatusRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionStatusRepository.java index 8503f7ed0..5fda8b9a3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionStatusRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionStatusRepository.java @@ -17,6 +17,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; /** @@ -63,4 +65,21 @@ public interface ActionStatusRepository */ @EntityGraph(value = "ActionStatus.withMessages", type = EntityGraphType.LOAD) Page getByActionId(Pageable pageReq, Long actionId); + + /** + * Finds a filtered list of status messages for an action. + * + * @param pageable + * for page configuration + * @param actionId + * for which to get the status messages + * @param filter + * is the SQL like pattern to use for filtering out or excluding + * the messages + * + * @return Page with found status messages. + */ + @Query("SELECT message FROM JpaActionStatus actionstatus JOIN actionstatus.messages message WHERE actionstatus.action.id = :actionId AND message NOT LIKE :filter") + Page findMessagesByActionIdAndMessageNotLike(final Pageable pageable, @Param("actionId") Long actionId, + @Param("filter") String filter); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java index 99ffba6ca..cd5fedb5e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.repository.jpa; import java.net.URI; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -59,6 +60,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; @@ -522,4 +524,25 @@ public class JpaControllerManagement implements ControllerManagement { return actionStatusRepository.findByActionId(pageReq, actionId); } + @Override + public List getActionHistoryMessages(final Long actionId, final int messageCount) { + // Just return empty list in case messageCount is zero. + if (messageCount == 0) { + return new ArrayList<>(); + } + + // For negative and large value of messageCount, limit the number of + // messages. + int limit = messageCount < 0 || messageCount >= RepositoryConstants.MAX_ACTION_HISTORY_MSG_COUNT + ? RepositoryConstants.MAX_ACTION_HISTORY_MSG_COUNT : messageCount; + + PageRequest pageable = new PageRequest(0, limit, new Sort(Direction.DESC, "occurredAt")); + Page messages = actionStatusRepository.findMessagesByActionIdAndMessageNotLike(pageable, actionId, + RepositoryConstants.SERVER_MESSAGE_PREFIX + "%"); + + LOG.debug("Retrieved {} message(s) from action history for action {}.", messages.getNumberOfElements(), + actionId); + + return messages.getContent(); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java index 05a6b81d5..7e4abfe9d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java @@ -46,6 +46,7 @@ import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import ru.yandex.qatools.allure.annotations.Description; @@ -620,4 +621,25 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { assertThat(targetManagement.getControllerAttributes(controllerId)).as("Controller Attributes are wrong") .isEqualTo(testData); } + + @Test + @Description("Test to verify the storage and retrieval of action history.") + public void findMessagesByActionStatusId() { + final DistributionSet testDs = testdataFactory.createDistributionSet("1"); + final List testTarget = testdataFactory.createTargets(1); + + final Long actionId = assignDistributionSet(testDs, testTarget).getActions().get(0); + + controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId) + .status(Action.Status.RUNNING).messages(Lists.newArrayList("proceeding message 1"))); + controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId) + .status(Action.Status.RUNNING).messages(Lists.newArrayList("proceeding message 2"))); + + final List messages = controllerManagement.getActionHistoryMessages(actionId, 2); + + assertThat(deploymentManagement.findActionStatusByAction(PAGE, actionId).getTotalElements()) + .as("Two action-states in total").isEqualTo(3L); + assertThat(messages.get(0)).as("Message of action-status").isEqualTo("proceeding message 2"); + assertThat(messages.get(1)).as("Message of action-status").isEqualTo("proceeding message 1"); + } }