Add REST method for update rollout (#1749)

* adds PUT method for updating name and description of a rollout
* restrict RolloutUpdate to changing only name and description
* small refactoring

Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-06-24 09:16:39 +03:00
committed by GitHub
parent 297775f539
commit 40f99962d2
20 changed files with 321 additions and 343 deletions

View File

@@ -10,7 +10,6 @@
package org.eclipse.hawkbit.mgmt.json.model.rollout;
import java.util.List;
import java.util.Optional;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -63,18 +62,18 @@ import com.fasterxml.jackson.annotation.JsonProperty;
},
"startAt" : 1682408570791
}""")
public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEntity {
public class MgmtRolloutRestRequestBodyPost extends AbstractMgmtRolloutConditionsEntity {
@Schema(description = "Target filter query language expression", example = "id==targets-*")
private String targetFilterQuery;
@Schema(description = "The ID of distributionset of this rollout", example = "6")
@Schema(description = "The ID of distribution set of this rollout", example = "6")
private long distributionSetId;
@Schema(description = "The amount of groups the rollout should split targets into", example = "5")
private Integer amountGroups;
@Schema(description = "Forcetime in milliseconds", example = "1691065781929")
@Schema(description = "Force time in milliseconds", example = "1691065781929")
private Long forcetime;
@Schema(description = "Start at timestamp of Rollout", example = "1691065780929")

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.mgmt.json.model.rollout;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.eclipse.hawkbit.mgmt.json.model.MgmtNamedEntity;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroup;
import java.util.List;
/**
* Model for request containing a rollout body e.g. in a POST request of
* creating a rollout via REST API.
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(example = """
{
"name" : "exampleRollout",
"description" : "Rollout for all named targets"
}""")
public class MgmtRolloutRestRequestBodyPut extends MgmtNamedEntity {
}

View File

@@ -17,7 +17,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
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.rollout.MgmtRolloutRestRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroupResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
import org.eclipse.hawkbit.rest.json.model.ExceptionInfo;
@@ -28,6 +29,8 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
/**
@@ -144,7 +147,7 @@ public interface MgmtRolloutRestApi {
/**
* Handles the POST request for creating rollout.
*
* @param rolloutRequestBody
* @param rolloutCreateBody
* the rollout body to be created.
* @return In case rollout could successful created the ResponseEntity with
* status code 201 with the successfully created rollout. In any
@@ -161,7 +164,7 @@ public interface MgmtRolloutRestApi {
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403",
description = "Insufficient permissions, entity is not allowed to be changed (i.e. read-only) or " +
"data volume restriction applies.",
"data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@@ -180,7 +183,50 @@ public interface MgmtRolloutRestApi {
@PostMapping(value = MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING, consumes = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE })
ResponseEntity<MgmtRolloutResponseBody> create(MgmtRolloutRestRequestBody rolloutRequestBody);
ResponseEntity<MgmtRolloutResponseBody> create(MgmtRolloutRestRequestBodyPost rolloutCreateBody);
/**
* Handles the POST request for creating rollout.
*
* @param rolloutUpdateBody
* the rollout body with details for update.
* @return In case rollout could successful updated the ResponseEntity with
* status code 200 with the successfully created rollout. In any
* failure the JsonResponseExceptionHandler is handling the
* response.
*/
@Operation(summary = "Update Rollout", description = "Handles the UPDATE request for a single " +
"Rollout. Required permission: UPDATE_ROLLOUT")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", description = "Distribution Set not found.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another " +
"user in another request at the same time. You may retry your modification request.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not " +
"supported by the server for this resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further " +
"attempts and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@PutMapping(value = MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING + "/{rolloutId}", consumes = {
MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = {
MediaType.APPLICATION_JSON_VALUE, MediaTypes.HAL_JSON_VALUE })
ResponseEntity<MgmtRolloutResponseBody> update(@PathVariable("rolloutId") Long rolloutId,
@RequestBody MgmtRolloutRestRequestBodyPut rolloutUpdateBody);
/**
* Handles the request for approving a rollout.

View File

@@ -22,7 +22,8 @@ import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutCondition.Conditio
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutErrorAction;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutErrorAction.ErrorAction;
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.rollout.MgmtRolloutRestRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPut;
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;
@@ -33,6 +34,7 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
import org.eclipse.hawkbit.repository.builder.RolloutUpdate;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
@@ -123,22 +125,30 @@ final class MgmtRolloutMapper {
return body;
}
static RolloutCreate fromRequest(final EntityFactory entityFactory, final MgmtRolloutRestRequestBody restRequest,
static RolloutCreate fromRequest(final EntityFactory entityFactory, final MgmtRolloutRestRequestBodyPost restRequest,
final DistributionSet distributionSet) {
return entityFactory.rollout().create().name(restRequest.getName()).description(restRequest.getDescription())
.dynamic(restRequest.isDynamic())
.set(distributionSet).targetFilterQuery(restRequest.getTargetFilterQuery())
return entityFactory.rollout().create()
.name(restRequest.getName())
.description(restRequest.getDescription())
.distributionSetId(distributionSet)
.targetFilterQuery(restRequest.getTargetFilterQuery())
.actionType(MgmtRestModelMapper.convertActionType(restRequest.getType()))
.forcedTime(restRequest.getForcetime()).startAt(restRequest.getStartAt())
.weight(restRequest.getWeight());
.weight(restRequest.getWeight())
.dynamic(restRequest.isDynamic());
}
static RolloutUpdate fromRequest(final EntityFactory entityFactory, final MgmtRolloutRestRequestBodyPut restRequest, final long rolloutId) {
return entityFactory.rollout().update(rolloutId)
.name(restRequest.getName())
.description(restRequest.getDescription());
}
static RolloutCreate fromRetriedRollout(final EntityFactory entityFactory, final Rollout rollout) {
return entityFactory.rollout().create()
.name(rollout.getName().concat("_retry"))
.description(rollout.getDescription())
.set(rollout.getDistributionSet())
.distributionSetId(rollout.getDistributionSet())
.targetFilterQuery("failedrollout==".concat(String.valueOf(rollout.getId())))
.actionType(rollout.getActionType())
.forcedTime(rollout.getForcedTime())

View File

@@ -19,7 +19,8 @@ import jakarta.validation.ValidationException;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
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.rollout.MgmtRolloutRestRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroup;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroupResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
@@ -130,9 +131,8 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
@Override
public ResponseEntity<MgmtRolloutResponseBody> create(
@RequestBody final MgmtRolloutRestRequestBody rolloutRequestBody) {
// first check the given RSQL query if it's well formed, otherwise and
@RequestBody final MgmtRolloutRestRequestBodyPost rolloutRequestBody) {
// first check the given RSQL query if it's well-formed, otherwise and
// exception is thrown
final String targetFilterQuery = rolloutRequestBody.getTargetFilterQuery();
if (targetFilterQuery == null) {
@@ -170,9 +170,8 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
return ResponseEntity.status(HttpStatus.CREATED).body(MgmtRolloutMapper.toResponseRollout(rollout, true));
}
private Optional<Boolean> isConfirmationRequiredForGroup(final MgmtRolloutGroup group,
final MgmtRolloutRestRequestBody request) {
final MgmtRolloutRestRequestBodyPost request) {
if (group.getConfirmationRequired() != null) {
return Optional.of(group.getConfirmationRequired());
} else if (request.getConfirmationRequired() != null) {
@@ -181,6 +180,15 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
return Optional.empty();
}
@Override
public ResponseEntity<MgmtRolloutResponseBody> update(
@PathVariable("rolloutId") final Long rolloutId,
@RequestBody final MgmtRolloutRestRequestBodyPut rolloutUpdateBody) {
final Rollout updated =
rolloutManagement.update(MgmtRolloutMapper.fromRequest(entityFactory, rolloutUpdateBody, rolloutId));
return ResponseEntity.ok(MgmtRolloutMapper.toResponseRollout(updated, true));
}
@Override
public ResponseEntity<Void> approve(@PathVariable("rolloutId") final Long rolloutId, final String remark) {
rolloutManagement.approveOrDeny(rolloutId, Rollout.ApprovalDecision.APPROVED, remark);

View File

@@ -20,6 +20,7 @@ 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.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -56,6 +57,7 @@ import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch;
import org.eclipse.hawkbit.repository.test.util.WithUser;
import org.eclipse.hawkbit.rest.util.JsonBuilder;
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
import org.json.JSONObject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -270,6 +272,31 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
}
@Test
@Description("Testing that rollout can be updated")
void updateRollout() throws Exception {
testdataFactory.createTargets(4, "rollout", "description");
final DistributionSet dsA = testdataFactory.createDistributionSet("");
// create a running rollout for the created targets
final Rollout rollout = rolloutManagement.create(
entityFactory
.rollout()
.create()
.name("rollout1")
.distributionSetId(dsA.getId())
.targetFilterQuery("controllerId==rollout*"),
4, false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
mvc.perform(put("/rest/v1/rollouts/" + rollout.getId()).content(
new JSONObject().put("name", "newName").put("description", "newDesc").toString()
).contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andExpect(jsonPath("$.name", equalTo("newName")))
.andExpect(jsonPath("$.description", equalTo("newDesc")));
}
@Test
@Description("Testing the empty list is returned if no rollout exists")
void noRolloutReturnsEmptyList() throws Exception {
@@ -286,7 +313,7 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
// create rollout including the created targets with prefix 'rollout'
final Rollout rollout = rolloutManagement.create(
entityFactory.rollout().create().name("rollout1").set(dsA.getId())
entityFactory.rollout().create().name("rollout1").distributionSetId(dsA.getId())
.targetFilterQuery("controllerId==rollout*"),
4, false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
@@ -305,7 +332,7 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
// create a running rollout for the created targets
final Rollout rollout = rolloutManagement.create(
entityFactory.rollout().create().name("rollout1").set(dsA.getId())
entityFactory.rollout().create().name("rollout1").distributionSetId(dsA.getId())
.targetFilterQuery("controllerId==rollout*"),
4, false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
@@ -345,13 +372,13 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
// create a running rollout for the created targets
final Rollout rollout = rolloutManagement.create(
entityFactory.rollout().create().name("rollout1").set(dsA.getId())
entityFactory.rollout().create().name("rollout1").distributionSetId(dsA.getId())
.targetFilterQuery("controllerId==rollout*"),
4, false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
rolloutManagement.create(
entityFactory.rollout().create().name("rollout2").set(dsA.getId())
entityFactory.rollout().create().name("rollout2").distributionSetId(dsA.getId())
.targetFilterQuery("controllerId==rollout*"),
4, false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
@@ -399,7 +426,7 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
.rollout()
.create()
.name("rollout1")
.set(dsA.getId())
.distributionSetId(dsA.getId())
.targetFilterQuery("controllerId==rollout*"),
4, false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
@@ -431,7 +458,7 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
// create rollout including the created targets with prefix 'rollout'
final Rollout rollout = rolloutManagement.create(
entityFactory.rollout().create().name("rollout1").set(dsA.getId())
entityFactory.rollout().create().name("rollout1").distributionSetId(dsA.getId())
.targetFilterQuery("controllerId==rollout*"),
4, false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
@@ -982,7 +1009,7 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
// create rollout including the created targets with prefix 'rollout'
final Rollout rollout = rolloutManagement.create(
entityFactory.rollout().create().name("rollout1").set(dsA.getId())
entityFactory.rollout().create().name("rollout1").distributionSetId(dsA.getId())
.targetFilterQuery("controllerId==rollout*"),
4, confirmationRequired, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
@@ -1328,7 +1355,7 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
// create a running rollout for the created targets
final Rollout rollout = rolloutManagement.create(
entityFactory.rollout().create().name("rollout1").set(dsA.getId())
entityFactory.rollout().create().name("rollout1").distributionSetId(dsA.getId())
.targetFilterQuery("controllerId==rollout*"),
4, false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
@@ -1509,7 +1536,7 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
private Rollout createRollout(final String name, final int amountGroups, final long distributionSetId,
final String targetFilterQuery, final boolean confirmationRequired) {
final Rollout rollout = rolloutManagement.create(
entityFactory.rollout().create().name(name).set(distributionSetId).targetFilterQuery(targetFilterQuery),
entityFactory.rollout().create().name(name).distributionSetId(distributionSetId).targetFilterQuery(targetFilterQuery),
amountGroups, confirmationRequired, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());