Add resource collection /rest/v1/actions to Management REST API (#1299)

* Initial commit

Signed-off-by: Stefan Behl <stefan.behl@bosch.io>

* Added filtering by RSQL

Signed-off-by: Stefan Behl <stefan.behl@bosch.io>

* Support for filtering actions by distribution set, target, rollout

* Added REST docs

* Fixed REST docs

* Introduce a config property which allows to disable the actions endpoint

* Introduce representation mode parameter

* Adapt REST docs

* Incorporate review findings

* Adapt REST docs

* Improve unit tests

* Minor improvements

* Fix REST docs

* Fix REST docs

* Fix PR review findings

Signed-off-by: Stefan Behl <stefan.behl@bosch.io>
This commit is contained in:
Stefan Behl
2023-01-10 17:19:05 +01:00
committed by GitHub
parent 27872282f5
commit d7796121d1
19 changed files with 1046 additions and 37 deletions

View File

@@ -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<MgmtAction> toResponse(final Collection<Action> 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);
}
}

View File

@@ -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<PagedList<MgmtAction>> 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<Action> 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));
}
}

View File

@@ -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<MgmtRolloutResponseBody> toResponseRollout(final List<Rollout> rollouts) {
return toResponseRollout(rollouts, false);
}
static List<MgmtRolloutResponseBody> toResponseRollout(final List<Rollout> 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());

View File

@@ -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<Rollout> 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<MgmtRolloutResponseBody> 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<MgmtRolloutResponseBody> rest = MgmtRolloutMapper.toResponseRollout(findRolloutsAll.getContent(),
repMode == MgmtRepresentationMode.FULL);
return ResponseEntity.ok(new PagedList<>(rest, findRolloutsAll.getTotalElements()));
}

View File

@@ -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;

View File

@@ -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<Action> 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<Action> 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<Action> 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<Action> 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<Action> generateTargetWithTwoUpdatesWithOneOverride(final String knownTargetId) {
return generateTargetWithTwoUpdatesWithOneOverrideWithMaintenanceWindow(knownTargetId, null, null, null);
}
private List<Action> generateTargetWithTwoUpdatesWithOneOverrideWithMaintenanceWindow(final String knownTargetId,
final String schedule, final String duration, final String timezone) {
final Target target = testdataFactory.createTarget(knownTargetId);
final Iterator<DistributionSet> sets = testdataFactory.createDistributionSets(2).iterator();
final DistributionSet one = sets.next();
final DistributionSet two = sets.next();
// Update
if (schedule == null) {
final List<Target> 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<Target> 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<Action> 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();
}
}