Introduce basic functionality for invalidation of distributionsets (#1179)

* Basic DS invalidation functionality

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Add checks for valid/complete DS

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Stop rollouts + auto assignments when invalidating a DS

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Add methods to count AAs + rollouts for invalidation

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Small refactoring for DS management

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Add invalidation functionality to REST API

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Fix update stopped rollouts status

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Add various tests

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Introduce countActionsForInvalidation

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Fix event tests with incomplete DS

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Add H2 migration script

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Fix action count method

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Fix REST documentation tests

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Change flyway version number

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Add lock for DS invalidation + adapt tests

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Move concurrency test to own class

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Handle possible InterruptedException

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Fix concurrency test

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Use one transaction for all invalidations

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Add invalidate endpoint to REST docu

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Execute invalidation in transaction when actions are cancelled

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Check that distribution set is valid when editing/creating metadata

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Remove all changes in UI

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Add DB migration files for all databases

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Implement review findings

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Move DS invalidation to own class to check permissions for single steps

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Move invalidation count methods to management classes

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>

* Fix failing tests

Signed-off-by: Sebastian Firsching <sebastian.firsching@bosch-si.com>
This commit is contained in:
Sebastian Firsching
2021-09-30 15:26:36 +02:00
committed by GitHub
parent b25e118e6c
commit 825cb64448
66 changed files with 2413 additions and 511 deletions

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2021 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.json.model.distributionset;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* Definition of the action cancel type for the distribution set invalidation
* via REST management API.
*
*/
public enum MgmtCancelationType {
/**
* Actions will be soft canceled.
*/
SOFT("soft"),
/**
* Actions will be force quit.
*/
FORCE("force"),
/**
* No actions will be canceled.
*/
NONE("none");
private final String name;
private MgmtCancelationType(final String name) {
this.name = name;
}
@JsonValue
public String getName() {
return name;
}
}

View File

@@ -49,6 +49,17 @@ public class MgmtDistributionSet extends MgmtNamedEntity {
@JsonProperty
private boolean deleted;
@JsonProperty
private boolean valid;
public boolean isValid() {
return valid;
}
public void setValid(final boolean valid) {
this.valid = valid;
}
public boolean isDeleted() {
return deleted;
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2021 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.json.model.distributionset;
import javax.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* A json annotated rest model for invalidate DistributionSet requests.
*
*/
public class MgmtInvalidateDistributionSetRequestBody {
@NotNull
@JsonProperty
private MgmtCancelationType actionCancelationType;
@JsonProperty
private boolean cancelRollouts;
public MgmtCancelationType getActionCancelationType() {
return actionCancelationType;
}
public void setActionCancelationType(final MgmtCancelationType actionCancelationType) {
this.actionCancelationType = actionCancelationType;
}
public boolean isCancelRollouts() {
return cancelRollouts;
}
public void setCancelRollouts(final boolean cancelRollouts) {
this.cancelRollouts = cancelRollouts;
}
}

View File

@@ -10,12 +10,15 @@ package org.eclipse.hawkbit.mgmt.rest.api;
import java.util.List;
import javax.validation.Valid;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtInvalidateDistributionSetRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
@@ -378,4 +381,19 @@ public interface MgmtDistributionSetRestApi {
@RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) int pagingOffsetParam,
@RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) int pagingLimitParam,
@RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) String sortParam);
/**
* Invalidates a distribution set
*
* @param distributionSetId
* the ID of the distribution set to invalidate
* @param invalidateRequestBody
* the definition if rollouts and actions should be canceled
* @return status OK if the invalidation was successful
*/
@PostMapping(value = "/{distributionSetId}/invalidate", consumes = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE })
ResponseEntity<Void> invalidateDistributionSet(@PathVariable("distributionSetId") Long distributionSetId,
@Valid MgmtInvalidateDistributionSetRequestBody invalidateRequestBody);
}

View File

@@ -110,6 +110,7 @@ public final class MgmtDistributionSetMapper {
response.setComplete(distributionSet.isComplete());
response.setType(distributionSet.getType().getKey());
response.setDeleted(distributionSet.isDeleted());
response.setValid(distributionSet.isValid());
distributionSet.getModules()
.forEach(module -> response.getModules().add(MgmtSoftwareModuleMapper.toResponse(module)));

View File

@@ -9,17 +9,21 @@
package org.eclipse.hawkbit.mgmt.rest.resource;
import java.util.AbstractMap.SimpleEntry;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import javax.validation.Valid;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtInvalidateDistributionSetRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
@@ -29,6 +33,7 @@ import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
@@ -40,6 +45,7 @@ import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation;
import org.eclipse.hawkbit.repository.model.DistributionSetMetadata;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.Target;
@@ -81,11 +87,14 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
private final SystemSecurityContext systemSecurityContext;
private final DistributionSetInvalidationManagement distributionSetInvalidationManagement;
MgmtDistributionSetResource(final SoftwareModuleManagement softwareModuleManagement,
final TargetManagement targetManagement, final TargetFilterQueryManagement targetFilterQueryManagement,
final DeploymentManagement deployManagament, final SystemManagement systemManagement,
final EntityFactory entityFactory, final DistributionSetManagement distributionSetManagement,
final SystemSecurityContext systemSecurityContext) {
final SystemSecurityContext systemSecurityContext,
final DistributionSetInvalidationManagement distributionSetInvalidationManagement) {
this.softwareModuleManagement = softwareModuleManagement;
this.targetManagement = targetManagement;
this.targetFilterQueryManagement = targetFilterQueryManagement;
@@ -94,6 +103,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
this.entityFactory = entityFactory;
this.distributionSetManagement = distributionSetManagement;
this.systemSecurityContext = systemSecurityContext;
this.distributionSetInvalidationManagement = distributionSetInvalidationManagement;
}
@Override
@@ -125,7 +135,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
@Override
public ResponseEntity<MgmtDistributionSet> getDistributionSet(
@PathVariable("distributionSetId") final Long distributionSetId) {
final DistributionSet foundDs = findDistributionSetWithExceptionIfNotFound(distributionSetId);
final DistributionSet foundDs = distributionSetManagement.getOrElseThrowException(distributionSetId);
final MgmtDistributionSet response = MgmtDistributionSetMapper.toResponse(foundDs);
MgmtDistributionSetMapper.addLinks(foundDs, response);
@@ -206,7 +216,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
@RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) {
// check if distribution set exists otherwise throw exception
// immediately
findDistributionSetWithExceptionIfNotFound(distributionSetId);
distributionSetManagement.getOrElseThrowException(distributionSetId);
final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam);
final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam);
@@ -254,11 +264,10 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
final List<Entry<String, Long>> offlineAssignments = assignments.stream()
.map(assignment -> new SimpleEntry<String, Long>(assignment.getId(), distributionSetId))
.collect(Collectors.toList());
return ResponseEntity
.ok(MgmtDistributionSetMapper
.toResponse(deployManagament.offlineAssignedDistributionSets(offlineAssignments)));
return ResponseEntity.ok(MgmtDistributionSetMapper
.toResponse(deployManagament.offlineAssignedDistributionSets(offlineAssignments)));
}
final List<DeploymentRequest> deploymentRequests = assignments.stream()
.map(assignment -> MgmtDeploymentRequestMapper.createAssignmentRequest(assignment, distributionSetId))
.collect(Collectors.toList());
@@ -374,8 +383,14 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
softwaremodules.getTotalElements()));
}
private DistributionSet findDistributionSetWithExceptionIfNotFound(final Long distributionSetId) {
return distributionSetManagement.get(distributionSetId)
.orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, distributionSetId));
@Override
public ResponseEntity<Void> invalidateDistributionSet(
@PathVariable("distributionSetId") final Long distributionSetId,
@Valid @RequestBody final MgmtInvalidateDistributionSetRequestBody invalidateRequestBody) {
distributionSetInvalidationManagement
.invalidateDistributionSet(new DistributionSetInvalidation(Arrays.asList(distributionSetId),
MgmtRestModelMapper.convertCancelationType(invalidateRequestBody.getActionCancelationType()),
invalidateRequestBody.isCancelRollouts()));
return ResponseEntity.ok().build();
}
}

View File

@@ -11,7 +11,9 @@ package org.eclipse.hawkbit.mgmt.rest.resource;
import org.eclipse.hawkbit.mgmt.json.model.MgmtBaseEntity;
import org.eclipse.hawkbit.mgmt.json.model.MgmtNamedEntity;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtCancelationType;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType;
import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity;
@@ -48,10 +50,10 @@ public final class MgmtRestModelMapper {
/**
* Convert the given {@link MgmtActionType} into a corresponding repository
* {@link ActionType}.
*
*
* @param actionTypeRest
* the REST representation of the action type
*
*
* @return <null> or the repository action type
*/
public static ActionType convertActionType(final MgmtActionType actionTypeRest) {
@@ -76,10 +78,10 @@ public final class MgmtRestModelMapper {
/**
* Converts the given repository {@link ActionType} into a corresponding
* {@link MgmtActionType}.
*
*
* @param actionType
* the repository representation of the action type
*
*
* @return <null> or the REST action type
*/
public static MgmtActionType convertActionType(final ActionType actionType) {
@@ -100,4 +102,30 @@ public final class MgmtRestModelMapper {
throw new IllegalStateException("Action Type is not supported");
}
}
/**
* Converts the given repository {@link CancelationType} into a
* corresponding {@link MgmtCancelationType}.
*
* @param cancelationType
* the repository representation of the cancellation type
*
* @return <null> or the REST cancellation type
*/
public static CancelationType convertCancelationType(final MgmtCancelationType cancelationType) {
if (cancelationType == null) {
return null;
}
switch (cancelationType) {
case SOFT:
return CancelationType.SOFT;
case FORCE:
return CancelationType.FORCE;
case NONE:
return CancelationType.NONE;
default:
throw new IllegalStateException("Action Cancelation Type is not supported");
}
}
}

View File

@@ -110,7 +110,8 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
// exception is thrown
targetFilterQueryManagement.verifyTargetFilterQuerySyntax(rolloutRequestBody.getTargetFilterQuery());
final DistributionSet distributionSet = findDistributionSetOrThrowException(rolloutRequestBody);
final DistributionSet distributionSet = distributionSetManagement
.getValidAndComplete(rolloutRequestBody.getDistributionSetId());
final RolloutGroupConditions rolloutGroupConditions = MgmtRolloutMapper.fromRequest(rolloutRequestBody, true);
final RolloutCreate create = MgmtRolloutMapper.fromRequest(entityFactory, rolloutRequestBody, distributionSet);
@@ -234,10 +235,4 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
final List<MgmtTarget> rest = MgmtTargetMapper.toResponse(rolloutGroupTargets.getContent());
return ResponseEntity.ok(new PagedList<>(rest, rolloutGroupTargets.getTotalElements()));
}
private DistributionSet findDistributionSetOrThrowException(final MgmtRolloutRestRequestBody rolloutRequestBody) {
return this.distributionSetManagement.get(rolloutRequestBody.getDistributionSetId()).orElseThrow(
() -> new EntityNotFoundException(DistributionSet.class, rolloutRequestBody.getDistributionSetId()));
}
}

View File

@@ -40,8 +40,12 @@ import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetMetadata;
import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
import org.eclipse.hawkbit.repository.test.util.WithUser;
import org.eclipse.hawkbit.rest.util.JsonBuilder;
@@ -1350,4 +1354,37 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr
assertThat(actions).size().isEqualTo(1);
assertThat(actions.get(0).getWeight()).get().isEqualTo(weight);
}
@Test
@Description("Verify invalidation of distribution sets that removes distribution sets from auto assignments, stops rollouts and cancels assignments")
public void invalidateDistributionSet() throws Exception {
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
final List<Target> targets = testdataFactory.createTargets(5, "invalidateDistributionSet");
assignDistributionSet(distributionSet, targets);
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
.create(entityFactory.targetFilterQuery().create().name("invalidateDistributionSet").query("name==*")
.autoAssignDistributionSet(distributionSet));
final Rollout rollout = testdataFactory.createRolloutByVariables("invalidateDistributionSet", "desc", 2,
"name==*", distributionSet, "50", "80");
final JSONObject jsonObject = new JSONObject();
jsonObject.put("actionCancelationType", "soft");
jsonObject.put("cancelRollouts", true);
mvc.perform(post("/rest/v1/distributionsets/{ds}/invalidate", distributionSet.getId())
.content(jsonObject.toString()).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
assertThat(targetFilterQueryManagement.get(targetFilterQuery.getId()).get().getAutoAssignDistributionSet())
.isNull();
assertThat(rolloutManagement.get(rollout.getId()).get().getStatus()).isIn(RolloutStatus.STOPPING,
RolloutStatus.FINISHED);
for (final Target target : targets) {
assertThat(targetManagement.get(target.getId()).get().getUpdateStatus())
.isEqualTo(TargetUpdateStatus.PENDING);
assertThat(deploymentManagement.findActionsByTarget(target.getControllerId(), PageRequest.of(0, 100))
.getNumberOfElements()).isEqualTo(1);
assertThat(deploymentManagement.findActionsByTarget(target.getControllerId(), PageRequest.of(0, 100))
.getContent().get(0).getStatus()).isEqualTo(Status.CANCELING);
}
}
}

View File

@@ -25,9 +25,10 @@ import java.util.List;
import org.eclipse.hawkbit.exception.SpServerError;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException;
import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignDistributionSetException;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.DistributionSet;
@@ -294,7 +295,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte
assertThat(exceptionInfo.getExceptionClass()).isEqualTo(MessageNotReadableException.class.getName());
assertThat(exceptionInfo.getErrorCode()).isEqualTo(SpServerError.SP_REST_BODY_NOT_READABLE.getKey());
}
@Test
@Description("Ensures that the creation of a target filter query based on an invalid RSQL query results in a HTTP Bad Request error (400).")
public void createTargetFilterWithInvalidQuery() throws Exception {
@@ -324,7 +325,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte
post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId() + "/autoAssignDS")
.content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON))
.andDo(print()).andExpect(status().isForbidden())
.andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(AssignmentQuotaExceededException.class.getName())))
.andExpect(
jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(AssignmentQuotaExceededException.class.getName())))
.andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey())));
}
@@ -356,7 +358,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte
mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId())
.content("{\"query\":\"controllerId==target*\"}").contentType(MediaType.APPLICATION_JSON))
.andDo(print()).andExpect(status().isForbidden())
.andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(AssignmentQuotaExceededException.class.getName())))
.andExpect(
jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(AssignmentQuotaExceededException.class.getName())))
.andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey())));
}
@@ -470,9 +473,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte
.content("{\"id\":" + incompleteDistributionSet.getId() + "}").contentType(MediaType.APPLICATION_JSON))
.andDo(print()).andExpect(status().isBadRequest())
.andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS,
equalTo(InvalidAutoAssignDistributionSetException.class.getName())))
.andExpect(jsonPath(JSON_PATH_ERROR_CODE,
equalTo(SpServerError.SP_AUTO_ASSIGN_DISTRIBUTION_SET_INVALID.getKey())));
equalTo(IncompleteDistributionSetException.class.getName())))
.andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_DS_INCOMPLETE.getKey())));
}
@Step
@@ -483,11 +485,9 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte
mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS")
.content("{\"id\":" + softDeletedDs.getId() + "}").contentType(MediaType.APPLICATION_JSON))
.andDo(print()).andExpect(status().isBadRequest())
.andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS,
equalTo(InvalidAutoAssignDistributionSetException.class.getName())))
.andExpect(jsonPath(JSON_PATH_ERROR_CODE,
equalTo(SpServerError.SP_AUTO_ASSIGN_DISTRIBUTION_SET_INVALID.getKey())));
.andDo(print()).andExpect(status().isNotFound())
.andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(EntityNotFoundException.class.getName())))
.andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_REPO_ENTITY_NOT_EXISTS.getKey())));
}
@Test

View File

@@ -83,11 +83,13 @@ public class ResponseExceptionHandler {
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_MAINTENANCE_SCHEDULE_INVALID, HttpStatus.BAD_REQUEST);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_TARGET_ATTRIBUTES_INVALID, HttpStatus.BAD_REQUEST);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_AUTO_ASSIGN_ACTION_TYPE_INVALID, HttpStatus.BAD_REQUEST);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_AUTO_ASSIGN_DISTRIBUTION_SET_INVALID, HttpStatus.BAD_REQUEST);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED, HttpStatus.FORBIDDEN);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_MULTIASSIGNMENT_NOT_ENABLED, HttpStatus.BAD_REQUEST);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_NO_WEIGHT_PROVIDED_IN_MULTIASSIGNMENT_MODE, HttpStatus.BAD_REQUEST);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_TARGET_TYPE_IN_USE, HttpStatus.CONFLICT);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_INVALID, HttpStatus.BAD_REQUEST);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_INCOMPLETE, HttpStatus.BAD_REQUEST);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_STOP_ROLLOUT_FAILED, HttpStatus.LOCKED);
}
private static HttpStatus getStatusOrDefault(final SpServerError error) {

View File

@@ -830,6 +830,54 @@ include::../errors/429.adoc[]
|===
== POST /rest/v1/distributionsets/{distributionSetId}/invalidate
=== Implementation Notes
Invalidate a distribution set. Once a distribution set is invalidated, it can not be valid again. An invalidated distribution set cannot be assigned to targets anymore. The distribution set that is going to be invalidated will be removed from all auto assignments. Furthermore, the user can choose to cancel all rollouts and (force) cancel all actions connected to this distribution set. Required permission: UPDATE_REPOSITORY
=== Invalidate a distribution set
==== Curl
include::{snippets}/distributionsets/invalidate/curl-request.adoc[]
==== Request URL
include::{snippets}/distributionsets/invalidate/http-request.adoc[]
==== Request path parameter
include::{snippets}/distributionsets/invalidate/path-parameters.adoc[]
==== Request fields
include::{snippets}/distributionsets/invalidate/request-fields.adoc[]
=== Response (Status 200)
==== Response example
include::{snippets}/distributionsets/invalidate/http-response.adoc[]
=== Error responses
|===
| HTTP Status Code | Reason | Response Model
include::../errors/400.adoc[]
include::../errors/401.adoc[]
include::../errors/403.adoc[]
include::../errors/404.adoc[]
include::../errors/405.adoc[]
include::../errors/406.adoc[]
include::../errors/409.adoc[]
include::../errors/415.adoc[]
include::../errors/429.adoc[]
|===
== Additional content
[[error-body]]

View File

@@ -89,8 +89,9 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati
protected String host = "management-api.host";
/**
* The generated REST docs snippets will be outputted to an own resource folder.
* The child class has to specify the name of that output folder where to put its corresponding snippets.
* The generated REST docs snippets will be outputted to an own resource
* folder. The child class has to specify the name of that output folder
* where to put its corresponding snippets.
*
* @return the name of the resource folder
*/
@@ -101,8 +102,8 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati
this.document = document(getResourceName() + "/{method-name}", preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()));
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(MockMvcRestDocumentation.documentationConfiguration(restDocContext).uris()
.withScheme("https").withHost(host + ".com").withPort(443))
.apply(MockMvcRestDocumentation.documentationConfiguration(restDocContext).uris().withScheme("https")
.withHost(host + ".com").withPort(443))
.alwaysDo(this.document).addFilter(filterHttpResponse).build();
arrayPrefix = "[]";
}
@@ -163,33 +164,36 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati
protected Target createTargetByGivenNameWithAttributes(final String name, final boolean inSync,
final boolean timeforced, final DistributionSet distributionSet) {
return createTargetByGivenNameWithAttributes(name, inSync, timeforced, distributionSet, null, null, null, false);
return createTargetByGivenNameWithAttributes(name, inSync, timeforced, distributionSet, null, null, null,
false);
}
protected Target createTargetByGivenNameWithAttributes(final String name, final boolean inSync,
final boolean timeforced, final DistributionSet distributionSet, final boolean createRollout) {
return createTargetByGivenNameWithAttributes(name, inSync, timeforced, distributionSet, null, null, null, createRollout);
return createTargetByGivenNameWithAttributes(name, inSync, timeforced, distributionSet, null, null, null,
createRollout);
}
protected Target createTargetByGivenNameWithAttributes(final String name, final boolean inSync,
final boolean timeforced, final DistributionSet distributionSet, final String maintenanceWindowSchedule,
final String maintenanceWindowDuration, final String maintenanceWindowTimeZone, final boolean createRollout) {
final String maintenanceWindowDuration, final String maintenanceWindowTimeZone,
final boolean createRollout) {
final Target savedTarget = targetManagement.create(entityFactory.target().create().controllerId(name)
.status(TargetUpdateStatus.UNKNOWN).address("http://192.168.0.1").description("My name is " + name)
.lastTargetQuery(System.currentTimeMillis()));
final List<Target> updatedTargets;
if (createRollout) {
final Rollout rollout = testdataFactory.createRolloutByVariables("rollout", "rollout desc", 1,
"name==" + name, distributionSet, "50", "5", timeforced ? ActionType.TIMEFORCED : ActionType.FORCED,
isMultiAssignmentsEnabled() ? 600 : null);
// start the rollout and handle it
rolloutManagement.start(rollout.getId());
rolloutManagement.handleRollouts();
updatedTargets = Collections.singletonList(savedTarget);
} else {
@@ -208,7 +212,7 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati
updatedTargets = makeAssignment(deploymentRequestBuilder.build()).getAssignedEntity().stream()
.map(Action::getTarget).collect(Collectors.toList());
}
if (inSync) {
feedbackToByInSync(distributionSet);
}
@@ -318,6 +322,7 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati
.description(MgmtApiModelProperties.DS_REQUIRED_STEP),
fieldWithPath(arrayPrefix + "complete").description(MgmtApiModelProperties.DS_COMPLETE),
fieldWithPath(arrayPrefix + "deleted").description(ApiModelPropertiesGeneric.DELETED),
fieldWithPath(arrayPrefix + "valid").description(MgmtApiModelProperties.DS_VALID),
fieldWithPath(arrayPrefix + "version").description(MgmtApiModelProperties.VERSION),
fieldWithPath(arrayPrefix + "_links.self").ignored(), fieldWithPath(arrayPrefix + "modules").ignored());

View File

@@ -67,6 +67,8 @@ public final class MgmtApiModelProperties {
public static final String DS_ALREADY_ASSIGNED_TARGETS = "Targets that had this distribution set already assigned (in \"offline\" case this includes targets that have arbitrary updates running)";
public static final String DS_TOTAL_ASSIGNED_TARGETS = "Overall assigned as part of this request.";
public static final String DS_ID = "Id of the distribution set.";
public static final String DS_INVALIDATION_ACTION_CANCELATION_TYPE = "Type of cancelation for actions referring to the given distribution set.";
public static final String DS_INVALIDATION_CANCEL_ROLLOUTS = "Defines if rollouts referring to this distribution set should be canceled.";
// Target
public static final String INSTALLED_AT = "Installation time of current installed DistributionSet.";
@@ -184,6 +186,8 @@ public final class MgmtApiModelProperties {
public static final String DS_COMPLETE = "True of the distribution set software module setup is complete as defined by the distribution set type.";
public static final String DS_VALID = "True by default and false after the distribution set is invalidated by the user.";
public static final String DS_TYPE_MANDATORY_MODULES = "Mandatory module type IDs.";
public static final String DS_TYPE_OPTIONAL_MODULES = "Optional module type IDs.";

View File

@@ -379,8 +379,7 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat
parameterWithName("distributionSetId").description(ApiModelPropertiesGeneric.ITEM_ID)),
requestParameters(parameterWithName("offline")
.description(MgmtApiModelProperties.OFFLINE_UPDATE).optional()),
requestFields(
requestFieldWithPath("[].id").description(ApiModelPropertiesGeneric.ITEM_ID),
requestFields(requestFieldWithPath("[].id").description(ApiModelPropertiesGeneric.ITEM_ID),
requestFieldWithPathMandatoryInMultiAssignMode("[].weight")
.description(MgmtApiModelProperties.ASSIGNMENT_WEIGHT)
.type(JsonFieldType.NUMBER).attributes(key("value").value("0 - 1000")),
@@ -395,8 +394,8 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat
optionalRequestFieldWithPath("[].maintenanceWindow.timezone")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE),
optionalRequestFieldWithPath("[].type")
.description(MgmtApiModelProperties.ASSIGNMENT_TYPE)
.attributes(key("value").value("['soft', 'forced','timeforced', 'downloadonly']"))),
.description(MgmtApiModelProperties.ASSIGNMENT_TYPE).attributes(
key("value").value("['soft', 'forced','timeforced', 'downloadonly']"))),
responseFields(
fieldWithPath("assigned").description(MgmtApiModelProperties.DS_NEW_ASSIGNED_TARGETS),
fieldWithPath("alreadyAssigned").type(JsonFieldType.NUMBER)
@@ -445,8 +444,7 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat
mockMvc.perform(delete(
MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING
+ "/{distributionSetId}/assignedSM/{softwareModuleId}",
set.getId(), set.findFirstModuleByType(osType).get().getId())
.contentType(MediaType.APPLICATION_JSON))
set.getId(), set.findFirstModuleByType(osType).get().getId()).contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andDo(this.document.document(pathParameters(
parameterWithName("distributionSetId").description(ApiModelPropertiesGeneric.ITEM_ID),
@@ -667,4 +665,28 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat
optionalRequestFieldWithPath("[]value")
.description(MgmtApiModelProperties.META_DATA_VALUE))));
}
@Test
@Description("Invalidates a distribution set. Required Permission: " + SpPermission.UPDATE_REPOSITORY)
public void invalidate() throws Exception {
final DistributionSet testDS = testdataFactory.createDistributionSet();
final JSONObject jsonObject = new JSONObject();
jsonObject.put("actionCancelationType", "soft");
jsonObject.put("cancelRollouts", true);
mockMvc.perform(post(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/{distributionSetId}/invalidate",
testDS.getId()).content(jsonObject.toString()).contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andDo(
this.document.document(
pathParameters(parameterWithName("distributionSetId")
.description(ApiModelPropertiesGeneric.ITEM_ID)),
requestFields(
requestFieldWithPath("actionCancelationType")
.description(
MgmtApiModelProperties.DS_INVALIDATION_ACTION_CANCELATION_TYPE)
.attributes(key("value").value("['force','soft','none']")),
optionalRequestFieldWithPath("cancelRollouts")
.description(MgmtApiModelProperties.DS_INVALIDATION_CANCEL_ROLLOUTS))));
}
}