From b6834e9ee2473d7d412c0035f675e0b737ef0f6d Mon Sep 17 00:00:00 2001 From: Dominik Herbst Date: Wed, 16 Nov 2016 09:26:50 +0100 Subject: [PATCH] Semi automatic Rollouts with fine groups definition (#337) * Rollout Mgmt API accepts now extended Group definition. Filling Reollout Groups with Targets is now a scheduled task. Signed-off-by: Dominik Herbst * Fire RolloutGroupCreated event and fix db migration. Signed-off-by: Dominik Herbst * Fill groups now excludes targets in own group Signed-off-by: Dominik Herbst * Starting of Rollouts as scheduled task Signed-off-by: Dominik Herbst * Finished implementation of new Rollout starting proccess Signed-off-by: Dominik Herbst * Reset last check on status change and fixed unused imports Signed-off-by: Dominik Herbst * Code quality improvements Signed-off-by: Dominik Herbst * Reworked start of scheduled Actions. Improved code quality. Signed-off-by: Dominik Herbst --- .../scenarios/ConfigurableScenario.java | 2 +- .../CreateStartedRolloutExample.java | 2 +- .../hawkbit/exception/SpServerError.java | 9 +- .../hawkbit/repository/TargetFields.java | 16 +- .../AbstractMgmtRolloutConditionsEntity.java | 60 ++ .../model/rollout/MgmtRolloutErrorAction.java | 27 +- .../rollout/MgmtRolloutRestRequestBody.java | 76 +-- .../model/rolloutgroup/MgmtRolloutGroup.java | 40 ++ .../MgmtRolloutGroupResponseBody.java | 24 +- .../mgmt/rest/api/MgmtRolloutRestApi.java | 3 +- .../mgmt/rest/resource/MgmtRolloutMapper.java | 90 ++- .../rest/resource/MgmtRolloutResource.java | 31 +- .../resource/MgmtRolloutResourceTest.java | 149 ++++- .../repository/DeploymentManagement.java | 32 +- .../hawkbit/repository/RolloutManagement.java | 138 ++-- .../hawkbit/repository/TargetManagement.java | 48 +- .../RolloutVerificationException.java | 63 ++ .../hawkbit/repository/model/Rollout.java | 4 + .../repository/model/RolloutGroup.java | 74 ++- .../model/RolloutGroupConditions.java | 16 +- .../repository/jpa/ActionRepository.java | 36 +- .../jpa/JpaDeploymentManagement.java | 70 +- .../repository/jpa/JpaRolloutManagement.java | 603 ++++++++++++------ .../repository/jpa/JpaTargetManagement.java | 33 + .../hawkbit/repository/jpa/RolloutHelper.java | 246 +++++++ .../jpa/RolloutTargetGroupRepository.java | 10 + .../repository/jpa/model/JpaRolloutGroup.java | 39 +- .../jpa/rollout/RolloutScheduler.java | 5 +- ...artNextGroupRolloutGroupSuccessAction.java | 10 +- .../specifications/TargetSpecifications.java | 42 ++ .../H2/V1_10_0__advanced_rolloutgroup__H2.sql | 4 + .../V1_10_0__advanced_rolloutgroup__MYSQL.sql | 4 + .../repository/jpa/RolloutManagementTest.java | 393 +++++++++++- .../jpa/rsql/RSQLActionFieldsTest.java | 4 + .../hawkbit/rest/util/JsonBuilder.java | 59 +- .../rollout/AddUpdateRolloutWindowLayout.java | 2 +- .../ui/rollout/rollout/RolloutListGrid.java | 2 +- 37 files changed, 2009 insertions(+), 457 deletions(-) create mode 100644 hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/AbstractMgmtRolloutConditionsEntity.java create mode 100644 hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rolloutgroup/MgmtRolloutGroup.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RolloutVerificationException.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutHelper.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_10_0__advanced_rolloutgroup__H2.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_10_0__advanced_rolloutgroup__MYSQL.sql diff --git a/examples/hawkbit-example-mgmt-simulator/src/main/java/org/eclipse/hawkbit/mgmt/client/scenarios/ConfigurableScenario.java b/examples/hawkbit-example-mgmt-simulator/src/main/java/org/eclipse/hawkbit/mgmt/client/scenarios/ConfigurableScenario.java index e5663ea0b..302156ac5 100644 --- a/examples/hawkbit-example-mgmt-simulator/src/main/java/org/eclipse/hawkbit/mgmt/client/scenarios/ConfigurableScenario.java +++ b/examples/hawkbit-example-mgmt-simulator/src/main/java/org/eclipse/hawkbit/mgmt/client/scenarios/ConfigurableScenario.java @@ -190,7 +190,7 @@ public class ConfigurableScenario { .getBody(); // start the created Rollout - rolloutResource.start(rolloutResponseBody.getRolloutId(), true); + rolloutResource.start(rolloutResponseBody.getRolloutId()); // wait until rollout is complete do { diff --git a/examples/hawkbit-example-mgmt-simulator/src/main/java/org/eclipse/hawkbit/mgmt/client/scenarios/CreateStartedRolloutExample.java b/examples/hawkbit-example-mgmt-simulator/src/main/java/org/eclipse/hawkbit/mgmt/client/scenarios/CreateStartedRolloutExample.java index 913d2eaa4..d46cbe0ea 100644 --- a/examples/hawkbit-example-mgmt-simulator/src/main/java/org/eclipse/hawkbit/mgmt/client/scenarios/CreateStartedRolloutExample.java +++ b/examples/hawkbit-example-mgmt-simulator/src/main/java/org/eclipse/hawkbit/mgmt/client/scenarios/CreateStartedRolloutExample.java @@ -102,7 +102,7 @@ public class CreateStartedRolloutExample { .getBody(); // start the created Rollout - rolloutResource.start(rolloutResponseBody.getRolloutId(), false); + rolloutResource.start(rolloutResponseBody.getRolloutId()); } } diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java index bda7a5d77..80e81e220 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java @@ -164,7 +164,12 @@ public enum SpServerError { /** * */ - SP_ROLLOUT_ILLEGAL_STATE("hawkbit.server.error.rollout.illegalstate", "The rollout is currently in the wrong state for the current operation"); + SP_ROLLOUT_ILLEGAL_STATE("hawkbit.server.error.rollout.illegalstate", "The rollout is in the wrong state for the requested operation"), + + /** + * + */ + SP_ROLLOUT_VERIFICATION_FAILED("hawkbit.server.error.rollout.verificationFailed", "The rollout configuration could not be verified successfully"); private final String key; private final String message; @@ -172,7 +177,7 @@ public enum SpServerError { /* * Repository side Error codes */ - private SpServerError(final String errorKey, final String message) { + SpServerError(final String errorKey, final String message) { key = errorKey; this.message = message; } diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java index 472c161ff..786979845 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java @@ -32,6 +32,14 @@ public enum TargetFields implements FieldNameProvider { * The description field. */ DESCRIPTION("description"), + /** + * The createdAt field. + */ + CREATEDAT("createdAt"), + /** + * The createdAt field. + */ + LASTMODIFIEDAT("lastModifiedAt"), /** * The controllerId field. */ @@ -75,19 +83,19 @@ public enum TargetFields implements FieldNameProvider { private List subEntityAttribues; private boolean mapField; - private TargetFields(final String fieldName) { + TargetFields(final String fieldName) { this(fieldName, false, Collections.emptyList()); } - private TargetFields(final String fieldName, final boolean isMapField) { + TargetFields(final String fieldName, final boolean isMapField) { this(fieldName, isMapField, Collections.emptyList()); } - private TargetFields(final String fieldName, final String... subEntityAttribues) { + TargetFields(final String fieldName, final String... subEntityAttribues) { this(fieldName, false, Arrays.asList(subEntityAttribues)); } - private TargetFields(final String fieldName, final boolean mapField, final List subEntityAttribues) { + TargetFields(final String fieldName, final boolean mapField, final List subEntityAttribues) { this.fieldName = fieldName; this.mapField = mapField; this.subEntityAttribues = subEntityAttribues; diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/AbstractMgmtRolloutConditionsEntity.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/AbstractMgmtRolloutConditionsEntity.java new file mode 100644 index 000000000..ee33fe9b8 --- /dev/null +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/AbstractMgmtRolloutConditionsEntity.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations 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.rollout; + +import org.eclipse.hawkbit.mgmt.json.model.MgmtNamedEntity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Model for defining Conditions and Actions + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class AbstractMgmtRolloutConditionsEntity extends MgmtNamedEntity { + + private MgmtRolloutCondition successCondition; + private MgmtRolloutSuccessAction successAction; + private MgmtRolloutCondition errorCondition; + private MgmtRolloutErrorAction errorAction; + + public MgmtRolloutCondition getSuccessCondition() { + return successCondition; + } + + public void setSuccessCondition(final MgmtRolloutCondition successCondition) { + this.successCondition = successCondition; + } + + public MgmtRolloutSuccessAction getSuccessAction() { + return successAction; + } + + public void setSuccessAction(final MgmtRolloutSuccessAction successAction) { + this.successAction = successAction; + } + + public MgmtRolloutCondition getErrorCondition() { + return errorCondition; + } + + public void setErrorCondition(final MgmtRolloutCondition errorCondition) { + this.errorCondition = errorCondition; + } + + public MgmtRolloutErrorAction getErrorAction() { + return errorAction; + } + + public void setErrorAction(final MgmtRolloutErrorAction errorAction) { + this.errorAction = errorAction; + } + +} diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutErrorAction.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutErrorAction.java index 71fdf3ac9..673c58a17 100644 --- a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutErrorAction.java +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutErrorAction.java @@ -13,14 +13,34 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; /** - * + * An action that runs when the error condition is met */ @JsonInclude(Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public class MgmtRolloutErrorAction { private ErrorAction action = ErrorAction.PAUSE; - private String expression = null; + private String expression; + + /** + * Creates a rollout error action + * + * @param action + * the action to run when th error condition is met + * @param expression + * the expression for the action + */ + public MgmtRolloutErrorAction(ErrorAction action, String expression) { + this.action = action; + this.expression = expression; + } + + /** + * Default constructor + */ + public MgmtRolloutErrorAction() { + // Instantiate default error action + } /** * @return the action @@ -52,6 +72,9 @@ public class MgmtRolloutErrorAction { this.expression = expression; } + /** + * Possible actions + */ public enum ErrorAction { PAUSE; } diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java index f3bdb2665..7e158f74e 100644 --- a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java @@ -8,12 +8,14 @@ */ package org.eclipse.hawkbit.mgmt.json.model.rollout; -import org.eclipse.hawkbit.mgmt.json.model.MgmtNamedEntity; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +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 @@ -21,66 +23,18 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; */ @JsonInclude(Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public class MgmtRolloutRestRequestBody extends MgmtNamedEntity { +public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEntity { private String targetFilterQuery; private long distributionSetId; - private int amountGroups = 1; - - private MgmtRolloutCondition successCondition = new MgmtRolloutCondition(); - private MgmtRolloutSuccessAction successAction = new MgmtRolloutSuccessAction(); - private MgmtRolloutCondition errorCondition = null; - private MgmtRolloutErrorAction errorAction = null; + private Integer amountGroups; private Long forcetime; private MgmtActionType type; - /** - * @return the finishCondition - */ - public MgmtRolloutCondition getSuccessCondition() { - return successCondition; - } - - /** - * @param successCondition - * the finishCondition to set - */ - public void setSuccessCondition(final MgmtRolloutCondition successCondition) { - this.successCondition = successCondition; - } - - /** - * @return the successAction - */ - public MgmtRolloutSuccessAction getSuccessAction() { - return successAction; - } - - /** - * @param successAction - * the successAction to set - */ - public void setSuccessAction(final MgmtRolloutSuccessAction successAction) { - this.successAction = successAction; - } - - /** - * @return the errorCondition - */ - public MgmtRolloutCondition getErrorCondition() { - return errorCondition; - } - - /** - * @param errorCondition - * the errorCondition to set - */ - public void setErrorCondition(final MgmtRolloutCondition errorCondition) { - this.errorCondition = errorCondition; - } + private List groups; /** * @return the targetFilterQuery @@ -115,7 +69,7 @@ public class MgmtRolloutRestRequestBody extends MgmtNamedEntity { /** * @return the groupSize */ - public int getAmountGroups() { + public Integer getAmountGroups() { return amountGroups; } @@ -123,7 +77,7 @@ public class MgmtRolloutRestRequestBody extends MgmtNamedEntity { * @param groupSize * the groupSize to set */ - public void setAmountGroups(final int groupSize) { + public void setAmountGroups(final Integer groupSize) { this.amountGroups = groupSize; } @@ -158,18 +112,16 @@ public class MgmtRolloutRestRequestBody extends MgmtNamedEntity { } /** - * @return the errorAction + * @return the List of defined Groups */ - public MgmtRolloutErrorAction getErrorAction() { - return errorAction; + public List getGroups() { + return groups; } /** - * @param errorAction - * the errorAction to set + * @param groups List of {@link MgmtRolloutGroup} */ - public void setErrorAction(final MgmtRolloutErrorAction errorAction) { - this.errorAction = errorAction; + public void setGroups(List groups) { + this.groups = groups; } - } diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rolloutgroup/MgmtRolloutGroup.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rolloutgroup/MgmtRolloutGroup.java new file mode 100644 index 000000000..c628f45e0 --- /dev/null +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rolloutgroup/MgmtRolloutGroup.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations 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.rolloutgroup; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import org.eclipse.hawkbit.mgmt.json.model.rollout.AbstractMgmtRolloutConditionsEntity; + +/** + * Model for defining the Attributes of a Rollout Group + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class MgmtRolloutGroup extends AbstractMgmtRolloutConditionsEntity { + + private String targetFilterQuery; + private Float targetPercentage; + + public String getTargetFilterQuery() { + return targetFilterQuery; + } + + public void setTargetFilterQuery(final String targetFilterQuery) { + this.targetFilterQuery = targetFilterQuery; + } + + public Float getTargetPercentage() { + return targetPercentage; + } + + public void setTargetPercentage(Float targetPercentage) { + this.targetPercentage = targetPercentage; + } +} diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rolloutgroup/MgmtRolloutGroupResponseBody.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rolloutgroup/MgmtRolloutGroupResponseBody.java index 5e740a1fb..7b69137d9 100644 --- a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rolloutgroup/MgmtRolloutGroupResponseBody.java +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rolloutgroup/MgmtRolloutGroupResponseBody.java @@ -8,20 +8,21 @@ */ package org.eclipse.hawkbit.mgmt.json.model.rolloutgroup; -import org.eclipse.hawkbit.mgmt.json.model.MgmtNamedEntity; - 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 java.util.HashMap; +import java.util.Map; + /** * Model for the rollout group annotated with json-annotations for easier * serialization and de-serialization. */ @JsonInclude(Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public class MgmtRolloutGroupResponseBody extends MgmtNamedEntity { +public class MgmtRolloutGroupResponseBody extends MgmtRolloutGroup { @JsonProperty(value = "id", required = true) private Long rolloutGroupId; @@ -29,6 +30,10 @@ public class MgmtRolloutGroupResponseBody extends MgmtNamedEntity { @JsonProperty(required = true) private String status; + private int totalTargets; + + private final Map totalTargetsPerStatus = new HashMap<>(); + /** * @return the rolloutGroupId */ @@ -58,4 +63,17 @@ public class MgmtRolloutGroupResponseBody extends MgmtNamedEntity { public void setStatus(final String status) { this.status = status; } + + public int getTotalTargets() { + return totalTargets; + } + + public void setTotalTargets(int totalTargets) { + this.totalTargets = totalTargets; + } + + public Map getTotalTargetsPerStatus() { + return totalTargetsPerStatus; + } + } diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java index 905bbad88..8c9267d80 100644 --- a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java @@ -88,8 +88,7 @@ public interface MgmtRolloutRestApi { */ @RequestMapping(method = RequestMethod.POST, value = "/{rolloutId}/start", produces = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) - ResponseEntity start(@PathVariable("rolloutId") final Long rolloutId, - @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_ASYNC, defaultValue = "false") final boolean startAsync); + ResponseEntity start(@PathVariable("rolloutId") final Long rolloutId); /** * Handles the POST request for pausing a rollout. diff --git a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java index 2a092851d..f817efa32 100644 --- a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java +++ b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java @@ -15,15 +15,20 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutCondition; import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutCondition.Condition; +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.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.MgmtRestConstants; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi; import org.eclipse.hawkbit.repository.EntityFactory; +import org.eclipse.hawkbit.repository.exception.ConstraintViolationException; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Rollout; @@ -76,8 +81,7 @@ final class MgmtRolloutMapper { } body.add(linkTo(methodOn(MgmtRolloutRestApi.class).getRollout(rollout.getId())).withRel("self")); - body.add(linkTo(methodOn(MgmtRolloutRestApi.class).start(rollout.getId(), false)).withRel("start")); - body.add(linkTo(methodOn(MgmtRolloutRestApi.class).start(rollout.getId(), true)).withRel("startAsync")); + body.add(linkTo(methodOn(MgmtRolloutRestApi.class).start(rollout.getId())).withRel("start")); body.add(linkTo(methodOn(MgmtRolloutRestApi.class).pause(rollout.getId())).withRel("pause")); body.add(linkTo(methodOn(MgmtRolloutRestApi.class).resume(rollout.getId())).withRel("resume")); body.add(linkTo(methodOn(MgmtRolloutRestApi.class).getRolloutGroups(rollout.getId(), @@ -88,12 +92,12 @@ final class MgmtRolloutMapper { } static Rollout fromRequest(final EntityFactory entityFactory, final MgmtRolloutRestRequestBody restRequest, - final DistributionSet distributionSet, final String filterQuery) { + final DistributionSet distributionSet) { final Rollout rollout = entityFactory.generateRollout(); rollout.setName(restRequest.getName()); rollout.setDescription(restRequest.getDescription()); rollout.setDistributionSet(distributionSet); - rollout.setTargetFilterQuery(filterQuery); + rollout.setTargetFilterQuery(restRequest.getTargetFilterQuery()); final ActionType convertActionType = MgmtRestModelMapper.convertActionType(restRequest.getType()); if (convertActionType != null) { rollout.setActionType(convertActionType); @@ -105,6 +109,44 @@ final class MgmtRolloutMapper { return rollout; } + static RolloutGroup fromRequest(final EntityFactory entityFactory, final MgmtRolloutGroup restRequest) { + final RolloutGroup group = entityFactory.generateRolloutGroup(); + group.setName(restRequest.getName()); + group.setDescription(restRequest.getDescription()); + + if (restRequest.getTargetFilterQuery() != null) { + group.setTargetFilterQuery(restRequest.getTargetFilterQuery()); + } + + final Float targetPercentage = restRequest.getTargetPercentage(); + if (targetPercentage == null) { + group.setTargetPercentage(100); + } else if (targetPercentage <= 0 || targetPercentage > 100) { + throw new ConstraintViolationException("Target percentage out of Range >0 - 100."); + } else { + group.setTargetPercentage(restRequest.getTargetPercentage()); + } + + if (restRequest.getSuccessCondition() != null) { + group.setSuccessCondition(mapFinishCondition(restRequest.getSuccessCondition().getCondition())); + group.setSuccessConditionExp(restRequest.getSuccessCondition().getExpression()); + } + if (restRequest.getSuccessAction() != null) { + group.setSuccessAction(map(restRequest.getSuccessAction().getAction())); + group.setSuccessActionExp(restRequest.getSuccessAction().getExpression()); + } + if (restRequest.getErrorCondition() != null) { + group.setErrorCondition(mapErrorCondition(restRequest.getErrorCondition().getCondition())); + group.setErrorConditionExp(restRequest.getErrorCondition().getExpression()); + } + if (restRequest.getErrorAction() != null) { + group.setErrorAction(map(restRequest.getErrorAction().getAction())); + group.setErrorActionExp(restRequest.getErrorAction().getExpression()); + } + + return group; + } + static List toResponseRolloutGroup(final List rollouts) { if (rollouts == null) { return Collections.emptyList(); @@ -123,6 +165,25 @@ final class MgmtRolloutMapper { body.setName(rolloutGroup.getName()); body.setRolloutGroupId(rolloutGroup.getId()); body.setStatus(rolloutGroup.getStatus().toString().toLowerCase()); + body.setTargetPercentage(rolloutGroup.getTargetPercentage()); + body.setTargetFilterQuery(rolloutGroup.getTargetFilterQuery()); + body.setTotalTargets(rolloutGroup.getTotalTargets()); + + body.setSuccessCondition(new MgmtRolloutCondition(map(rolloutGroup.getSuccessCondition()), + rolloutGroup.getSuccessConditionExp())); + body.setSuccessAction( + new MgmtRolloutSuccessAction(map(rolloutGroup.getSuccessAction()), rolloutGroup.getSuccessActionExp())); + + body.setErrorCondition( + new MgmtRolloutCondition(map(rolloutGroup.getErrorCondition()), rolloutGroup.getErrorConditionExp())); + body.setErrorAction( + new MgmtRolloutErrorAction(map(rolloutGroup.getErrorAction()), rolloutGroup.getErrorActionExp())); + + for (final TotalTargetCountStatus.Status status : TotalTargetCountStatus.Status.values()) { + body.getTotalTargetsPerStatus().put(status.name().toLowerCase(), + rolloutGroup.getTotalTargetCountStatus().getTotalTargetCountByStatus(status)); + } + body.add(linkTo(methodOn(MgmtRolloutRestApi.class).getRolloutGroup(rolloutGroup.getRollout().getId(), rolloutGroup.getId())).withRel("self")); return body; @@ -149,6 +210,13 @@ final class MgmtRolloutMapper { throw new IllegalArgumentException("Rollout group condition " + rolloutCondition + NOT_SUPPORTED); } + static Condition map(final RolloutGroupErrorCondition rolloutCondition) { + if (RolloutGroupErrorCondition.THRESHOLD.equals(rolloutCondition)) { + return Condition.THRESHOLD; + } + throw new IllegalArgumentException("Rollout group condition " + rolloutCondition + NOT_SUPPORTED); + } + static RolloutGroupErrorAction map(final ErrorAction action) { if (ErrorAction.PAUSE.equals(action)) { return RolloutGroupErrorAction.PAUSE; @@ -163,6 +231,20 @@ final class MgmtRolloutMapper { throw new IllegalArgumentException("Success Action " + action + NOT_SUPPORTED); } + static SuccessAction map(final RolloutGroupSuccessAction successAction) { + if (RolloutGroupSuccessAction.NEXTGROUP.equals(successAction)) { + return SuccessAction.NEXTGROUP; + } + throw new IllegalArgumentException("Rollout group success action " + successAction + NOT_SUPPORTED); + } + + static ErrorAction map(final RolloutGroupErrorAction errorAction) { + if (RolloutGroupErrorAction.PAUSE.equals(errorAction)) { + return ErrorAction.PAUSE; + } + throw new IllegalArgumentException("Rollout group error action " + errorAction + NOT_SUPPORTED); + } + private static String createIllegalArgumentLiteral(final Condition condition) { return "Condition " + condition + NOT_SUPPORTED; } diff --git a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java index 7144316b8..ea68b9753 100644 --- a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java +++ b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.mgmt.rest.resource; import java.util.List; +import java.util.stream.Collectors; import org.eclipse.hawkbit.mgmt.json.model.PagedList; import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutResponseBody; @@ -24,6 +25,7 @@ import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.RolloutVerificationException; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup; @@ -143,23 +145,30 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi { .successCondition(successCondition, successConditionExpr) .successAction(successAction, successActionExpr).errorCondition(errorCondition, errorConditionExpr) .errorAction(errorAction, errorActionExpr).build(); - final Rollout rollout = this.rolloutManagement.createRollout( - MgmtRolloutMapper.fromRequest(entityFactory, rolloutRequestBody, distributionSet, - rolloutRequestBody.getTargetFilterQuery()), - rolloutRequestBody.getAmountGroups(), rolloutGroupConditions); + + Rollout rollout = MgmtRolloutMapper.fromRequest(entityFactory, rolloutRequestBody, distributionSet); + + if (rolloutRequestBody.getGroups() != null) { + final List rolloutGroups = rolloutRequestBody.getGroups().stream() + .map(mgmtRolloutGroup -> MgmtRolloutMapper.fromRequest(entityFactory, mgmtRolloutGroup)) + .collect(Collectors.toList()); + rollout = rolloutManagement.createRollout(rollout, rolloutGroups, rolloutGroupConditions); + + } else if (rolloutRequestBody.getAmountGroups() != null) { + rollout = rolloutManagement.createRollout(rollout, rolloutRequestBody.getAmountGroups(), + rolloutGroupConditions); + + } else { + throw new RolloutVerificationException("Either 'amountGroups' or 'groups' must be defined in the request"); + } return ResponseEntity.status(HttpStatus.CREATED).body(MgmtRolloutMapper.toResponseRollout(rollout)); } @Override - public ResponseEntity start(@PathVariable("rolloutId") final Long rolloutId, - @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_ASYNC, defaultValue = "false") final boolean startAsync) { + public ResponseEntity start(@PathVariable("rolloutId") final Long rolloutId) { final Rollout rollout = findRolloutOrThrowException(rolloutId); - if (startAsync) { - this.rolloutManagement.startRolloutAsync(rollout); - } else { - this.rolloutManagement.startRollout(rollout); - } + this.rolloutManagement.startRollout(rollout); return ResponseEntity.ok().build(); } diff --git a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java index 0d64803cf..091861a61 100644 --- a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java +++ b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java @@ -18,6 +18,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; @@ -30,6 +31,7 @@ import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCondition; import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder; +import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.test.util.WithUser; import org.eclipse.hawkbit.rest.AbstractRestIntegrationTest; @@ -117,8 +119,85 @@ public class MgmtRolloutResourceTest extends AbstractRestIntegrationTest { @Test @Description("Testing that rollout can be created") public void createRollout() throws Exception { + targetManagement.createTargets(testdataFactory.generateTargets(20, "target", "rollout")); + final DistributionSet dsA = testdataFactory.createDistributionSet(""); - postRollout("rollout1", 10, dsA.getId(), "name==target1"); + postRollout("rollout1", 10, dsA.getId(), "id==target*"); + } + + @Test + @Description("Testing that rollout can be created with groups") + public void createRolloutWithGroupsDefinitions() throws Exception { + final DistributionSet dsA = testdataFactory.createDistributionSet("ro"); + + final int amountTargets = 10; + targetManagement.createTargets(testdataFactory.generateTargets(amountTargets, "ro-target", "rollout")); + + List rolloutGroups = new ArrayList<>(2); + final int percentTargetsInGroup1 = 20; + final int percentTargetsInGroup2 = 100; + + RolloutGroup group1 = entityFactory.generateRolloutGroup(); + group1.setName("Group1"); + group1.setDescription("Group1desc"); + group1.setTargetPercentage(percentTargetsInGroup1); + rolloutGroups.add(group1); + + RolloutGroup group2 = entityFactory.generateRolloutGroup(); + group2.setName("Group2"); + group2.setDescription("Group2desc"); + group2.setTargetPercentage(percentTargetsInGroup2); + rolloutGroups.add(group2); + + RolloutGroupConditions rolloutGroupConditions = new RolloutGroupConditionBuilder().build(); + + mvc.perform(post("/rest/v1/rollouts") + .content(JsonBuilder.rollout("rollout2", "desc", null, dsA.getId(), "id==ro-target*", + rolloutGroupConditions, rolloutGroups)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()).andReturn(); + + } + + @Test + @Description("Testing that no rollout with groups that have illegal percentages can be created") + public void createRolloutWithIllegalPercentages() throws Exception { + final DistributionSet dsA = testdataFactory.createDistributionSet("ro2"); + + final int amountTargets = 10; + targetManagement.createTargets(testdataFactory.generateTargets(amountTargets, "ro-target", "rollout")); + + List rolloutGroups = new ArrayList<>(2); + + RolloutGroup group1 = entityFactory.generateRolloutGroup(); + group1.setName("Group1"); + group1.setDescription("Group1desc"); + group1.setTargetPercentage(0); + rolloutGroups.add(group1); + + RolloutGroup group2 = entityFactory.generateRolloutGroup(); + group2.setName("Group2"); + group2.setDescription("Group2desc"); + group2.setTargetPercentage(100); + rolloutGroups.add(group2); + + RolloutGroupConditions rolloutGroupConditions = new RolloutGroupConditionBuilder().build(); + + mvc.perform(post("/rest/v1/rollouts") + .content(JsonBuilder.rollout("rollout4", "desc", null, dsA.getId(), "id==ro-target*", + rolloutGroupConditions, rolloutGroups)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.repo.constraintViolation"))); + + group1.setTargetPercentage(101); + mvc.perform(post("/rest/v1/rollouts") + .content(JsonBuilder.rollout("rollout4", "desc", null, dsA.getId(), "id==ro-target*", + rolloutGroupConditions, rolloutGroups)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.repo.constraintViolation"))); + } @Test @@ -133,20 +212,26 @@ public class MgmtRolloutResourceTest extends AbstractRestIntegrationTest { @Description("Testing that rollout paged list contains rollouts") public void rolloutPagedListContainsAllRollouts() throws Exception { final DistributionSet dsA = testdataFactory.createDistributionSet(""); + + targetManagement.createTargets(testdataFactory.generateTargets(20, "target", "rollout")); + // setup - create 2 rollouts - postRollout("rollout1", 10, dsA.getId(), "name==target1"); - postRollout("rollout2", 5, dsA.getId(), "name==target2"); + postRollout("rollout1", 10, dsA.getId(), "id==target*"); + postRollout("rollout2", 5, dsA.getId(), "id==target-0001*"); + + // Run here, because Scheduler is disabled during tests + rolloutManagement.checkCreatingRollouts(0); mvc.perform(get("/rest/v1/rollouts")).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andExpect(jsonPath("$.content", hasSize(2))).andExpect(jsonPath("$.total", equalTo(2))) .andExpect(jsonPath("content[0].name", equalTo("rollout1"))) .andExpect(jsonPath("content[0].status", equalTo("ready"))) - .andExpect(jsonPath("content[0].targetFilterQuery", equalTo("name==target1"))) + .andExpect(jsonPath("content[0].targetFilterQuery", equalTo("id==target*"))) .andExpect(jsonPath("content[0].distributionSetId", equalTo(dsA.getId().intValue()))) .andExpect(jsonPath("content[1].name", equalTo("rollout2"))) .andExpect(jsonPath("content[1].status", equalTo("ready"))) - .andExpect(jsonPath("content[1].targetFilterQuery", equalTo("name==target2"))) + .andExpect(jsonPath("content[1].targetFilterQuery", equalTo("id==target-0001*"))) .andExpect(jsonPath("content[1].distributionSetId", equalTo(dsA.getId().intValue()))); } @@ -154,9 +239,15 @@ public class MgmtRolloutResourceTest extends AbstractRestIntegrationTest { @Description("Testing that rollout paged list is limited by the query param limit") public void rolloutPagedListIsLimitedToQueryParam() throws Exception { final DistributionSet dsA = testdataFactory.createDistributionSet(""); + + targetManagement.createTargets(testdataFactory.generateTargets(20, "target", "rollout")); + // setup - create 2 rollouts - postRollout("rollout1", 10, dsA.getId(), "name==target1"); - postRollout("rollout2", 5, dsA.getId(), "name==target2"); + postRollout("rollout1", 10, dsA.getId(), "id==target*"); + postRollout("rollout2", 5, dsA.getId(), "id==target*"); + + // Run here, because Scheduler is disabled during tests + rolloutManagement.checkCreatingRollouts(0); mvc.perform(get("/rest/v1/rollouts?limit=1")).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) @@ -186,7 +277,7 @@ public class MgmtRolloutResourceTest extends AbstractRestIntegrationTest { } @Test - @Description("Testing that starting the rollout switches the state to running") + @Description("Testing that starting the rollout switches the state to starting and then to running") public void startingRolloutSwitchesIntoRunningState() throws Exception { // setup final int amountTargets = 20; @@ -200,6 +291,15 @@ public class MgmtRolloutResourceTest extends AbstractRestIntegrationTest { mvc.perform(post("/rest/v1/rollouts/{rolloutId}/start", rollout.getId())).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); + // check rollout is in starting state + mvc.perform(get("/rest/v1/rollouts/{rolloutId}", rollout.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("id", equalTo(rollout.getId().intValue()))) + .andExpect(jsonPath("status", equalTo("starting"))); + + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + // check rollout is in running state mvc.perform(get("/rest/v1/rollouts/{rolloutId}", rollout.getId())).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) @@ -222,6 +322,9 @@ public class MgmtRolloutResourceTest extends AbstractRestIntegrationTest { mvc.perform(post("/rest/v1/rollouts/{rolloutId}/start", rollout.getId())).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + // pausing rollout mvc.perform(post("/rest/v1/rollouts/{rolloutId}/pause", rollout.getId())).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); @@ -248,6 +351,9 @@ public class MgmtRolloutResourceTest extends AbstractRestIntegrationTest { mvc.perform(post("/rest/v1/rollouts/{rolloutId}/start", rollout.getId())).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + // pausing rollout mvc.perform(post("/rest/v1/rollouts/{rolloutId}/pause", rollout.getId())).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); @@ -278,6 +384,9 @@ public class MgmtRolloutResourceTest extends AbstractRestIntegrationTest { mvc.perform(post("/rest/v1/rollouts/{rolloutId}/start", rollout.getId())).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + // starting rollout - already started should lead into bad request mvc.perform(post("/rest/v1/rollouts/{rolloutId}/start", rollout.getId())).andDo(MockMvcResultPrinter.print()) .andExpect(status().isBadRequest()) @@ -316,6 +425,9 @@ public class MgmtRolloutResourceTest extends AbstractRestIntegrationTest { mvc.perform(post("/rest/v1/rollouts/{rolloutId}/start", rollout.getId())).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + // retrieve rollout groups from created rollout - 2 groups exists // (amountTargets / groupSize = 2) mvc.perform(get("/rest/v1/rollouts/{rolloutId}/deploygroups?sort=ID:ASC", rollout.getId())) @@ -409,6 +521,9 @@ public class MgmtRolloutResourceTest extends AbstractRestIntegrationTest { rolloutManagement.startRollout(rollout); + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + final RolloutGroup firstGroup = rolloutGroupManagement .findRolloutGroupsByRolloutId(rollout.getId(), new PageRequest(0, 1, Direction.ASC, "id")).getContent() .get(0); @@ -432,10 +547,12 @@ public class MgmtRolloutResourceTest extends AbstractRestIntegrationTest { final Rollout rollout = createRollout("rollout1", 4, dsA.getId(), "controllerId==rollout*"); // starting rollout - mvc.perform(post("/rest/v1/rollouts/{rolloutId}/start", rollout.getId()) - .param(MgmtRestConstants.REQUEST_PARAMETER_ASYNC, "true")).andDo(MockMvcResultPrinter.print()) + mvc.perform(post("/rest/v1/rollouts/{rolloutId}/start", rollout.getId())).andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + // check if running assertThat(doWithTimeout(() -> getRollout(rollout.getId()), result -> success(result), 60_000, 100)) .isNotNull(); @@ -549,19 +666,25 @@ public class MgmtRolloutResourceTest extends AbstractRestIntegrationTest { private void postRollout(final String name, final int groupSize, final long distributionSetId, final String targetFilterQuery) throws Exception { mvc.perform(post("/rest/v1/rollouts") - .content(JsonBuilder.rollout(name, "desc", groupSize, distributionSetId, targetFilterQuery, null)) + .content(JsonBuilder.rollout(name, "desc", groupSize, distributionSetId, targetFilterQuery, + new RolloutGroupConditionBuilder().build())) .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()).andReturn(); } private Rollout createRollout(final String name, final int amountGroups, final long distributionSetId, final String targetFilterQuery) { - final Rollout rollout = entityFactory.generateRollout(); + Rollout rollout = entityFactory.generateRollout(); rollout.setDistributionSet(distributionSetManagement.findDistributionSetById(distributionSetId)); rollout.setName(name); rollout.setTargetFilterQuery(targetFilterQuery); - return rolloutManagement.createRollout(rollout, amountGroups, new RolloutGroupConditionBuilder() + rollout = rolloutManagement.createRollout(rollout, amountGroups, new RolloutGroupConditionBuilder() .successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build()); + + // Run here, because Scheduler is disabled during tests + rolloutManagement.fillRolloutGroupsWithTargets(rolloutManagement.findRolloutById(rollout.getId())); + + return rolloutManagement.findRolloutById(rollout.getId()); } protected boolean success(final Rollout result) { diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java index b14cc9131..36ba6df24 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java @@ -276,25 +276,6 @@ public interface DeploymentManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) List findActionsByRolloutAndStatus(@NotNull Rollout rollout, @NotNull Action.Status actionStatus); - /** - * Retrieving all actions referring to a given rollout with a specific - * action as parent reference and a specific status. - * - * Finding all actions of a specific rolloutgroup parent relation. - * - * @param rollout - * the rollout the actions belong to - * @param rolloutGroupParent - * the parent rollout group the actions should reference - * @param actionStatus - * the status the actions have - * @return the actions referring a specific rollout and a specific parent - * rollout group in a specific status - */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) - List findActionsByRolloutGroupParentAndStatus(@NotNull Rollout rollout, - @NotNull RolloutGroup rolloutGroupParent, @NotNull Action.Status actionStatus); - /** * Retrieves all {@link Action}s of a specific target. * @@ -513,6 +494,19 @@ public interface DeploymentManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) Action startScheduledAction(@NotNull Long actionId); + /** + * Starts all scheduled actions of an RolloutGroup parent. + * + * @param rollout + * the rollout the actions belong to + * @param rolloutGroupParent + * the parent rollout group the actions should reference. null + * references the first group + * @return the amount of started actions + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + long startScheduledActionsByRolloutGroupParent(@NotNull Rollout rollout, RolloutGroup rolloutGroupParent); + /** * All {@link ActionStatus} entries in the repository. * diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java index 88cc36330..127e9565d 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java @@ -11,7 +11,6 @@ package org.eclipse.hawkbit.repository; import javax.validation.constraints.NotNull; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; -import org.eclipse.hawkbit.repository.event.remote.entity.RolloutGroupCreatedEvent; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; @@ -26,6 +25,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.security.access.prepost.PreAuthorize; +import java.util.List; + /** * RolloutManagement to control rollouts e.g. like creating, starting, resuming * and pausing rollouts. This service secures all the functionality based on the @@ -64,6 +65,30 @@ public interface RolloutManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) void checkRunningRollouts(long delayBetweenChecks); + /** + * Checking Rollouts that are currently being created with asynchronous + * assignment of targets to the Rollout Groups. + * + * @param delayBetweenChecks + * the time in milliseconds of the delay between the further and + * this check. This check is only applied if the last check is + * less than (lastcheck-delay). + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) + void checkCreatingRollouts(long delayBetweenChecks); + + /** + * Checking Rollouts that are currently being started with asynchronous + * creation of actions to the targets of a group. + * + * @param delayBetweenChecks + * the time in milliseconds of the delay between the further and + * this check. This check is only applied if the last check is + * less than (lastcheck-delay). + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) + void checkStartingRollouts(long delayBetweenChecks); + /** * Counts all {@link Rollout}s in the repository. * @@ -85,14 +110,17 @@ public interface RolloutManagement { /** * Persists a new rollout entity. The filter within the * {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets - * which are effected by this rollout to create. The targets will then be - * split up into groups. The size of the groups can be defined in the - * {@code groupSize} parameter. + * which are effected by this rollout to create. The amount of groups will + * be defined as equally sized. * * The rollout is not started. Only the preparation of the rollout is done, - * persisting and creating all the necessary groups. The Rollout and the - * groups are persisted in {@link RolloutStatus#READY} and - * {@link RolloutGroupStatus#READY} so they can be started + * creating and persisting all the necessary groups. The Rollout and the + * groups are persisted in {@link RolloutStatus#CREATING} and + * {@link RolloutGroupStatus#CREATING}. + * + * The RolloutScheduler will start to assign targets to the groups. Once all + * targets have been assigned to the groups, the rollout status is changed + * to {@link RolloutStatus#READY} so it can be started with * {@link #startRollout(Rollout)}. * * @param rollout @@ -112,40 +140,52 @@ public interface RolloutManagement { /** * Persists a new rollout entity. The filter within the - * {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets - * which are effected by this rollout to create. The creation of the rollout - * will be done synchronously and will be returned. The targets will then be - * split up into groups. The size of the groups can be defined in the - * {@code groupSize} parameter. - * - * The creation of the rollout groups is executed asynchronously due it - * might take some time to split up the targets into groups. The creation of - * the {@link RolloutGroup} is published as event - * {@link RolloutGroupCreatedEvent}. - * - * The rollout is in status {@link RolloutStatus#CREATING} until all rollout - * groups has been created and the targets are split up, then the rollout - * will change the status to {@link RolloutStatus#READY}. + * {@link Rollout#getTargetFilterQuery()} is used to filter the targets + * which are affected by this rollout. The given groups will be used to + * create the groups. * * The rollout is not started. Only the preparation of the rollout is done, - * persisting and creating all the necessary groups. The Rollout and the - * groups are persisted in {@link RolloutStatus#READY} and - * {@link RolloutGroupStatus#READY} so they can be started + * creating and persisting all the necessary groups. The Rollout and the + * groups are persisted in {@link RolloutStatus#CREATING} and + * {@link RolloutGroupStatus#CREATING}. + * + * The RolloutScheduler will start to assign targets to the groups. Once all + * targets have been assigned to the groups, the rollout status is changed + * to {@link RolloutStatus#READY} so it can be started with * {@link #startRollout(Rollout)}. * * @param rollout - * the rollout to be created - * @param amountGroup - * the number of groups should be created for the rollout and - * split up the targets + * the rollout entity to create + * @param groups + * optional definition of groups * @param conditions - * the rolloutgroup conditions and actions which should be - * applied for each {@link RolloutGroup} - * @return the created rollout entity in state - * {@link RolloutStatus#CREATING} + * the rollout group conditions and actions which should be + * applied for each {@link RolloutGroup} if not defined by the + * RolloutGroup itself + * @return the persisted rollout. + * + * @throws IllegalArgumentException + * in case the given groupSize is zero or lower. */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) - Rollout createRolloutAsync(@NotNull Rollout rollout, int amountGroup, @NotNull RolloutGroupConditions conditions); + Rollout createRollout(@NotNull Rollout rollout, @NotNull List groups, RolloutGroupConditions conditions); + + /** + * Can be called on a Rollout in {@link RolloutStatus#CREATING} to + * automatically fill it with targets. + * + * Works through all Rollout groups in {@link RolloutGroupStatus#CREATING} + * and fills them with remaining targets until the supposed amount of + * targets for the group is reached. Targets are added to a group when they + * match the overall {@link Rollout#getTargetFilterQuery()} and the + * {@link RolloutGroup#getTargetFilterQuery()} and not more than + * {@link RolloutGroup#getTargetPercentage()} are assigned to the group. + * + * @param rollout + * the rollout + */ + void fillRolloutGroupsWithTargets(final Rollout rollout); + /** * Retrieves all rollouts. @@ -284,12 +324,10 @@ public interface RolloutManagement { /** * Starts a rollout which has been created. The rollout must be in - * {@link RolloutStatus#READY} state. The according actions will be created - * for each affected target in the rollout. The actions of the first group - * will be started immediately {@link RolloutGroupStatus#RUNNING} as the - * other groups will be {@link RolloutGroupStatus#SCHEDULED} state. - * - * The rollout itself will be then also in {@link RolloutStatus#RUNNING}. + * {@link RolloutStatus#READY} state. The Rollout will be set into the + * {@link RolloutStatus#STARTING} state. The RolloutScheduler will ensure + * all actions are created and the first group is started. The rollout + * itself will be then also in {@link RolloutStatus#RUNNING}. * * @param rollout * the rollout to be started @@ -303,28 +341,6 @@ public interface RolloutManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) Rollout startRollout(@NotNull Rollout rollout); - /** - * Starts a rollout asynchronously which has been created. The rollout must - * be in {@link RolloutStatus#READY} state. The according actions will be - * created asynchronously for each affected target in the rollout. The - * actions of the first group will be started immediately - * {@link RolloutGroupStatus#RUNNING} as the other groups will be - * {@link RolloutGroupStatus#SCHEDULED} state. - * - * The rollout itself will be then also in {@link RolloutStatus#RUNNING}. - * - * @param rollout - * the rollout to be started - * - * @return the started rollout - * - * @throws RolloutIllegalStateException - * if given rollout is not in {@link RolloutStatus#READY}. Only - * ready rollouts can be started. - */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) - Rollout startRolloutAsync(@NotNull Rollout rollout); - /** * Update rollout details. * diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java index 62511e9ec..fbdf1ee0d 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java @@ -19,6 +19,7 @@ import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.Tag; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; @@ -268,7 +269,7 @@ public interface TargetManagement { * id of the {@link DistributionSet} * @param targetFilterQuery * {@link TargetFilterQuery} - * @return the found {@link TargetIdName}s + * @return a page of the found {@link Target}s */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) Page findAllTargetsByTargetFilterQueryAndNonDS(@NotNull Pageable pageRequest, Long distributionSetId, @@ -283,11 +284,54 @@ public interface TargetManagement { * id of the {@link DistributionSet} * @param targetFilterQuery * {@link TargetFilterQuery} - * @return the found {@link TargetIdName}s + * @return the count of found {@link Target}s */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) Long countTargetsByTargetFilterQueryAndNonDS(Long distributionSetId, @NotNull TargetFilterQuery targetFilterQuery); + /** + * Finds all targets for all the given parameter {@link TargetFilterQuery} + * and that are not assigned to one of the {@link RolloutGroup}s + * + * @param pageRequest + * the pageRequest to enhance the query for paging and sorting + * @param groups + * the list of {@link RolloutGroup}s + * @param targetFilterQuery + * RSQL filter + * @return a page of the found {@link Target}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + Page findAllTargetsByTargetFilterQueryAndNotInRolloutGroups(@NotNull Pageable pageRequest, + List groups, @NotNull String targetFilterQuery); + + /** + * Counts all targets for all the given parameter {@link TargetFilterQuery} + * and that are not assigned to one of the {@link RolloutGroup}s + * + * @param groups + * the list of {@link RolloutGroup}s + * @param targetFilterQuery + * RSQL filter + * @return count of the found {@link Target}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + Long countAllTargetsByTargetFilterQueryAndNotInRolloutGroups(List groups, + @NotNull String targetFilterQuery); + + /** + * Finds all targets of the provided {@link RolloutGroup} that have no + * Action for the RolloutGroup. + * + * @param pageRequest + * the pageRequest to enhance the query for paging and sorting + * @param group + * the {@link RolloutGroup} + * @return the found {@link Target}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + Page findAllTargetsInRolloutGroupWithoutAction(@NotNull Pageable pageRequest, @NotNull RolloutGroup group); + /** * retrieves {@link Target}s by the assigned {@link DistributionSet} without * details, i.e. NO {@link Target#getTags()} and {@link Target#getActions()} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RolloutVerificationException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RolloutVerificationException.java new file mode 100644 index 000000000..4dda248dd --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RolloutVerificationException.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations 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.repository.exception; + +import org.eclipse.hawkbit.exception.AbstractServerRtException; +import org.eclipse.hawkbit.exception.SpServerError; + +/** + * the {@link RolloutVerificationException} is thrown when a rollout or + * its groups get created or modified with a configuration that is + * not valid or can't be verified + * + */ +public class RolloutVerificationException extends AbstractServerRtException { + + private static final long serialVersionUID = 1L; + private static final SpServerError THIS_ERROR = SpServerError.SP_ROLLOUT_VERIFICATION_FAILED; + + /** + * Default constructor. + */ + public RolloutVerificationException() { + super(THIS_ERROR); + } + + /** + * Parameterized constructor. + * + * @param cause + * of the exception + */ + public RolloutVerificationException(final Throwable cause) { + super(THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + * @param cause + * of the exception + */ + public RolloutVerificationException(final String message, final Throwable cause) { + super(message, THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + */ + public RolloutVerificationException(final String message) { + super(message, THIS_ERROR); + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java index 72d7e7c53..e3373f4ea 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java @@ -146,13 +146,17 @@ public interface Rollout extends NamedEntity { /** * Rollout could not be created due to errors, might be a database * problem during asynchronous creating. + * @deprecated legacy status is not used anymore */ + @Deprecated ERROR_CREATING, /** * Rollout could not be started due to errors, might be database problem * during asynchronous starting. + * @deprecated legacy status is not used anymore */ + @Deprecated ERROR_STARTING; } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroup.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroup.java index 286085eba..5dcf7d67c 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroup.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroup.java @@ -152,6 +152,23 @@ public interface RolloutGroup extends NamedEntity { */ String getSuccessActionExp(); + /** + * @param successAction + * which is executed if the {@link RolloutGroupSuccessCondition} + * is met + */ + void setSuccessAction(RolloutGroupSuccessAction successAction); + + /** + * + * @param successActionExp + * a String representation of the expression to be evaluated by + * the {@link RolloutGroupSuccessAction} might be {@code null} if + * no expression must be set for the + * {@link RolloutGroupSuccessAction} + */ + void setSuccessActionExp(String successActionExp); + /** * @return the total amount of targets containing in this group */ @@ -169,10 +186,41 @@ public interface RolloutGroup extends NamedEntity { void setTotalTargetCountStatus(TotalTargetCountStatus totalTargetCountStatus); /** - * Rollout goup state machine. + * @return the target filter query, that is used to assign Targets to this + * Group + */ + String getTargetFilterQuery(); + + /** + * @param targetFilterQuery + * the target filter query, that is used to assign Targets to + * this Group. Can be null, if no restrictions should apply. + */ + void setTargetFilterQuery(String targetFilterQuery); + + /** + * @return the percentage of matching Targets that should be assigned to + * this Group + */ + float getTargetPercentage(); + + /** + * @param targetPercentage + * the percentage of matching Targets that should be assigned to + * this Group + */ + void setTargetPercentage(float targetPercentage); + + /** + * Rollout group state machine. * */ - public enum RolloutGroupStatus { + enum RolloutGroupStatus { + + /** + * Group has been defined, but not all targets have been assigned yet. + */ + CREATING, /** * Ready to start the group. @@ -198,18 +246,18 @@ public interface RolloutGroup extends NamedEntity { /** * Group is running. */ - RUNNING; + RUNNING } /** * The condition to evaluate if an group is success state. */ - public enum RolloutGroupSuccessCondition { + enum RolloutGroupSuccessCondition { THRESHOLD("thresholdRolloutGroupSuccessCondition"); private final String beanName; - private RolloutGroupSuccessCondition(final String beanName) { + RolloutGroupSuccessCondition(final String beanName) { this.beanName = beanName; } @@ -224,12 +272,12 @@ public interface RolloutGroup extends NamedEntity { /** * The condition to evaluate if an group is in error state. */ - public enum RolloutGroupErrorCondition { + enum RolloutGroupErrorCondition { THRESHOLD("thresholdRolloutGroupErrorCondition"); private final String beanName; - private RolloutGroupErrorCondition(final String beanName) { + RolloutGroupErrorCondition(final String beanName) { this.beanName = beanName; } @@ -242,14 +290,14 @@ public interface RolloutGroup extends NamedEntity { } /** - * The actions executed when the {@link RolloutGroup#errorCondition} is hit. + * The actions executed when the {@link RolloutGroup#getErrorCondition()} is hit. */ - public enum RolloutGroupErrorAction { + enum RolloutGroupErrorAction { PAUSE("pauseRolloutGroupAction"); private final String beanName; - private RolloutGroupErrorAction(final String beanName) { + RolloutGroupErrorAction(final String beanName) { this.beanName = beanName; } @@ -262,15 +310,15 @@ public interface RolloutGroup extends NamedEntity { } /** - * The actions executed when the {@link RolloutGroup#successCondition} is + * The actions executed when the {@link RolloutGroup#getSuccessCondition()} is * hit. */ - public enum RolloutGroupSuccessAction { + enum RolloutGroupSuccessAction { NEXTGROUP("startNextRolloutGroupAction"); private final String beanName; - private RolloutGroupSuccessAction(final String beanName) { + RolloutGroupSuccessAction(final String beanName) { this.beanName = beanName; } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroupConditions.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroupConditions.java index 618e93c4e..0b627fb6a 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroupConditions.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroupConditions.java @@ -18,14 +18,14 @@ import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCond * easily built. */ public class RolloutGroupConditions { - private RolloutGroupSuccessCondition successCondition; - private String successConditionExp; - private RolloutGroupSuccessAction successAction; - private String successActionExp; - private RolloutGroupErrorCondition errorCondition; - private String errorConditionExp; - private RolloutGroupErrorAction errorAction; - private String errorActionExp; + private RolloutGroupSuccessCondition successCondition = RolloutGroupSuccessCondition.THRESHOLD; + private String successConditionExp = "50"; + private RolloutGroupSuccessAction successAction = RolloutGroupSuccessAction.NEXTGROUP; + private String successActionExp = ""; + private RolloutGroupErrorCondition errorCondition = RolloutGroupErrorCondition.THRESHOLD; + private String errorConditionExp = "50"; + private RolloutGroupErrorAction errorAction = RolloutGroupErrorAction.PAUSE; + private String errorActionExp = ""; public RolloutGroupSuccessCondition getSuccessCondition() { return successCondition; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java index 7d9c9deb2..0cdd117f2 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java @@ -37,7 +37,6 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import org.springframework.hateoas.Identifiable; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; @@ -339,6 +338,8 @@ public interface ActionRepository extends BaseEntityRepository, * * Finding all actions of a specific rolloutgroup parent relation. * + * @param pageable + * page parameters * @param rollout * the rollout the actions belong to * @param rolloutGroupParent @@ -348,7 +349,38 @@ public interface ActionRepository extends BaseEntityRepository, * @return the actions referring a specific rollout and a specific parent * rolloutgroup in a specific status */ - List> findByRolloutAndRolloutGroupParentAndStatus(JpaRollout rollout, + Page findByRolloutAndRolloutGroupParentAndStatus(Pageable pageable, JpaRollout rollout, + JpaRolloutGroup rolloutGroupParent, Status actionStatus); + + /** + * Retrieving all actions referring to the first group of a rollout. + * + * @param pageable + * page parameters + * @param rollout + * the rollout the actions belong to + * @param actionStatus + * the status the actions have + * @return the actions referring a specific rollout and a specific parent + * rolloutgroup in a specific status + */ + Page findByRolloutAndRolloutGroupParentIsNullAndStatus(Pageable pageable, JpaRollout rollout, + Status actionStatus); + + /** + * Retrieving all actions referring to a given rollout group with a specific + * a specific status. + * + * @param rollout + * the rollout the actions belong to + * @param rolloutGroupParent + * the parent rollout group the actions should reference + * @param actionStatus + * the status the actions have + * @return the actions ids referring a specific rollout group and a specific + * status + */ + List findByRolloutAndRolloutGroupAndStatus(JpaRollout rollout, JpaRolloutGroup rolloutGroupParent, Status actionStatus); /** diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java index 5c45a2af3..111158a4a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java @@ -78,13 +78,18 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.AuditorAware; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.Modifying; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionTemplate; import org.springframework.validation.annotation.Validated; import com.google.common.collect.Lists; @@ -98,6 +103,11 @@ import com.google.common.collect.Lists; public class JpaDeploymentManagement implements DeploymentManagement { private static final Logger LOG = LoggerFactory.getLogger(JpaDeploymentManagement.class); + /** + * Maximum amount of Actions that are started at once. + */ + private static final int ACTION_PAGE_LIMIT = 1000; + @Autowired private EntityManager entityManager; @@ -134,6 +144,9 @@ public class JpaDeploymentManagement implements DeploymentManagement { @Autowired private VirtualPropertyReplacer virtualPropertyReplacer; + @Autowired + private PlatformTransactionManager txManager; + @Override @Transactional(isolation = Isolation.READ_COMMITTED) @Modifying @@ -482,13 +495,60 @@ public class JpaDeploymentManagement implements DeploymentManagement { }); } + @Override + @Modifying + @Transactional(isolation = Isolation.READ_COMMITTED) + public long startScheduledActionsByRolloutGroupParent(@NotNull final Rollout rollout, + final RolloutGroup rolloutGroupParent) { + long totalActionsCount = 0L; + long lastStartedActionsCount; + do { + lastStartedActionsCount = startScheduledActionsByRolloutGroupParentInNewTransaction(rollout, + rolloutGroupParent, ACTION_PAGE_LIMIT); + totalActionsCount += lastStartedActionsCount; + } while (lastStartedActionsCount > 0); + + return totalActionsCount; + } + + private long startScheduledActionsByRolloutGroupParentInNewTransaction(final Rollout rollout, + final RolloutGroup rolloutGroupParent, final int limit) { + final DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + def.setName("startScheduledActions"); + def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + return new TransactionTemplate(txManager, def).execute(status -> { + final Page rolloutGroupActions = findActionsByRolloutAndRolloutGroupParent(rollout, + rolloutGroupParent, limit); + + rolloutGroupActions.map(action -> (JpaAction) action).forEach(this::startScheduledAction); + + return rolloutGroupActions.getTotalElements(); + }); + } + + private Page findActionsByRolloutAndRolloutGroupParent(final Rollout rollout, + final RolloutGroup rolloutGroupParent, final int limit) { + JpaRollout jpaRollout = (JpaRollout) rollout; + JpaRolloutGroup jpaRolloutGroup = (JpaRolloutGroup) rolloutGroupParent; + PageRequest pageRequest = new PageRequest(0, limit); + if (rolloutGroupParent == null) { + return actionRepository.findByRolloutAndRolloutGroupParentIsNullAndStatus(pageRequest, jpaRollout, + Action.Status.SCHEDULED); + } else { + return actionRepository.findByRolloutAndRolloutGroupParentAndStatus(pageRequest, jpaRollout, + jpaRolloutGroup, Action.Status.SCHEDULED); + } + } + @Override @Modifying @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED) public Action startScheduledAction(final Long actionId) { - final JpaAction action = actionRepository.findById(actionId); + return startScheduledAction(action); + } + private Action startScheduledAction(final JpaAction action) { // check if we need to override running update actions final Set overrideObsoleteUpdateActions = overrideObsoleteUpdateActions( Collections.singletonList(action.getTarget().getId())); @@ -665,14 +725,6 @@ public class JpaDeploymentManagement implements DeploymentManagement { return actionStatusRepository.getByAction(pageReq, (JpaAction) action); } - @Override - public List findActionsByRolloutGroupParentAndStatus(final Rollout rollout, - final RolloutGroup rolloutGroupParent, final Action.Status actionStatus) { - return actionRepository.findByRolloutAndRolloutGroupParentAndStatus((JpaRollout) rollout, - (JpaRolloutGroup) rolloutGroupParent, actionStatus).stream().map(ident -> ident.getId()) - .collect(Collectors.toList()); - } - @Override public List findActionsByRolloutAndStatus(final Rollout rollout, final Action.Status actionStatus) { return actionRepository.findByRolloutAndStatus((JpaRollout) rollout, actionStatus); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java index eda694aab..042f7c9d5 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java @@ -11,21 +11,21 @@ package org.eclipse.hawkbit.repository.jpa; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; import java.util.stream.Collectors; import javax.persistence.EntityManager; +import org.apache.commons.lang3.StringUtils; import org.eclipse.hawkbit.repository.DeploymentManagement; -import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; import org.eclipse.hawkbit.repository.RolloutFields; import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.event.remote.entity.RolloutGroupCreatedEvent; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; +import org.eclipse.hawkbit.repository.exception.RolloutVerificationException; +import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; import org.eclipse.hawkbit.repository.jpa.model.JpaRollout_; @@ -44,7 +44,6 @@ import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCondition; import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; import org.eclipse.hawkbit.repository.model.Target; -import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.repository.model.TotalTargetCountActionStatus; import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; @@ -52,23 +51,23 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Direction; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.Modifying; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.Assert; import org.springframework.validation.annotation.Validated; @@ -81,6 +80,11 @@ import org.springframework.validation.annotation.Validated; public class JpaRolloutManagement implements RolloutManagement { private static final Logger LOGGER = LoggerFactory.getLogger(RolloutManagement.class); + /** + * Maximum amount of targets that are assigned to a Rollout Group in one transaction. + */ + private static final long TRANSACTION_TARGETS = 1000; + @Autowired private EntityManager entityManager; @@ -90,9 +94,6 @@ public class JpaRolloutManagement implements RolloutManagement { @Autowired private TargetManagement targetManagement; - @Autowired - private TargetRepository targetRepository; - @Autowired private RolloutGroupRepository rolloutGroupRepository; @@ -124,24 +125,7 @@ public class JpaRolloutManagement implements RolloutManagement { private VirtualPropertyReplacer virtualPropertyReplacer; @Autowired - @Qualifier("asyncExecutor") - private Executor executor; - - /* - * set which stores the rollouts which are asynchronously creating. This is - * necessary to verify rollouts which maybe stuck during creationg e.g. - * because of database interruption, failures or even application crash. - * !This is not cluster aware! - */ - private static final Set creatingRollouts = ConcurrentHashMap.newKeySet(); - - /* - * set which stores the rollouts which are asynchronously starting. This is - * necessary to verify rollouts which maybe stuck during starting e.g. - * because of database interruption, failures or even application crash. - * !This is not cluster aware! - */ - private static final Set startingRollouts = ConcurrentHashMap.newKeySet(); + private AfterTransactionCommitExecutor afterCommit; @Override public Page findAll(final Pageable pageable) { @@ -177,176 +161,406 @@ public class JpaRolloutManagement implements RolloutManagement { @Modifying public Rollout createRollout(final Rollout rollout, final int amountGroup, final RolloutGroupConditions conditions) { - final JpaRollout savedRollout = createRollout((JpaRollout) rollout, amountGroup); + RolloutHelper.verifyRolloutGroupParameter(amountGroup); + final JpaRollout savedRollout = createRollout((JpaRollout) rollout); return createRolloutGroups(amountGroup, conditions, savedRollout); } @Override @Transactional(isolation = Isolation.READ_UNCOMMITTED) @Modifying - public Rollout createRolloutAsync(final Rollout rollout, final int amountGroup, - final RolloutGroupConditions conditions) { - final JpaRollout savedRollout = createRollout((JpaRollout) rollout, amountGroup); - creatingRollouts.add(savedRollout.getName()); - // need to flush the entity manager here to get the ID of the rollout, - // because entity manager is set to FlushMode#Auto, entitymanager will - // flush the Target entity, due the indirect relationship to the Rollout - // entity without set an ID JPA is throwing a Invalid - // 'org.springframework.dao.InvalidDataAccessApiUsageException: During - // synchronization aect was found through a relationship that was not - // marked cascade PERSIST' - entityManager.flush(); - executor.execute(() -> { - try { - createRolloutGroupsInNewTransaction(amountGroup, conditions, savedRollout); - } finally { - creatingRollouts.remove(savedRollout.getName()); - } - }); - return savedRollout; + public Rollout createRollout(final Rollout rollout, + final List groups, + final RolloutGroupConditions conditions) { + RolloutHelper.verifyRolloutGroupParameter(groups.size()); + final JpaRollout savedRollout = createRollout((JpaRollout) rollout); + return createRolloutGroups(groups, conditions, savedRollout); } - private JpaRollout createRollout(final JpaRollout rollout, final int amountGroup) { - verifyRolloutGroupParameter(amountGroup); + private JpaRollout createRollout(final JpaRollout rollout) { + JpaRollout existingRollout = rolloutRepository.findByName(rollout.getName()); + if(existingRollout != null) { + throw new EntityAlreadyExistsException(existingRollout.getName()); + } + final Long totalTargets = targetManagement.countTargetByTargetFilterQuery(rollout.getTargetFilterQuery()); - rollout.setTotalTargets(totalTargets.longValue()); + if(totalTargets == 0) { + throw new RolloutVerificationException("Rollout does not match any existing targets"); + } + rollout.setTotalTargets(totalTargets); return rolloutRepository.save(rollout); } - private static void verifyRolloutGroupParameter(final int amountGroup) { - if (amountGroup <= 0) { - throw new IllegalArgumentException("the amountGroup must be greater than zero"); - } else if (amountGroup > 500) { - throw new IllegalArgumentException("the amountGroup must not be greater than 500"); - } - } - - private Rollout createRolloutGroupsInNewTransaction(final int amountOfGroups, - final RolloutGroupConditions conditions, final JpaRollout savedRollout) { - final DefaultTransactionDefinition def = new DefaultTransactionDefinition(); - def.setName("creatingRollout"); - def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - return new TransactionTemplate(txManager, def) - .execute(status -> createRolloutGroups(amountOfGroups, conditions, savedRollout)); - } - - /** - * Method for creating rollout groups and calculating group sizes. Group - * sizes are calculated by dividing the total count of targets through the - * amount of given groups. In same cases this will lead to less rollout - * groups than given by client. - * - * @param amountOfGroups - * the amount of groups - * @param conditions - * the rollout group conditions - * @param savedRollout - * the rollout - * @return the rollout with created groups - */ private Rollout createRolloutGroups(final int amountOfGroups, final RolloutGroupConditions conditions, - final JpaRollout savedRollout) { - int pageIndex = 0; - int groupIndex = 0; - final Long totalCount = savedRollout.getTotalTargets(); - final int groupSize = (int) Math.ceil((double) totalCount / (double) amountOfGroups); + final Rollout rollout) { + RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING); + RolloutHelper.verifyRolloutGroupConditions(conditions); + + final JpaRollout savedRollout = (JpaRollout) rollout; + RolloutGroup lastSavedGroup = null; - while (pageIndex < totalCount) { - groupIndex++; - final String nameAndDesc = "group-" + groupIndex; + for (int i = 0; i < amountOfGroups; i++) { + final String nameAndDesc = "group-" + (i + 1); final JpaRolloutGroup group = new JpaRolloutGroup(); group.setName(nameAndDesc); group.setDescription(nameAndDesc); group.setRollout(savedRollout); group.setParent(lastSavedGroup); + group.setStatus(RolloutGroupStatus.CREATING); + group.setSuccessCondition(conditions.getSuccessCondition()); group.setSuccessConditionExp(conditions.getSuccessConditionExp()); + + group.setSuccessAction(conditions.getSuccessAction()); + group.setSuccessActionExp(conditions.getSuccessActionExp()); + group.setErrorCondition(conditions.getErrorCondition()); group.setErrorConditionExp(conditions.getErrorConditionExp()); + group.setErrorAction(conditions.getErrorAction()); group.setErrorActionExp(conditions.getErrorActionExp()); - final JpaRolloutGroup savedGroup = rolloutGroupRepository.save(group); + group.setTargetPercentage(1.0F / (amountOfGroups - i) * 100); - final Slice targetGroup = targetManagement.findTargetsAll(savedRollout.getTargetFilterQuery(), - new OffsetBasedPageRequest(pageIndex, groupSize, new Sort(Direction.ASC, "id"))); - savedGroup.setTotalTargets(targetGroup.getContent().size()); + lastSavedGroup = rolloutGroupRepository.save(group); + publishRolloutGroupCreatedEventAfterCommit(lastSavedGroup); - lastSavedGroup = savedGroup; - - targetGroup - .forEach(target -> rolloutTargetGroupRepository.save(new RolloutTargetGroup(savedGroup, target))); - eventPublisher.publishEvent(new RolloutGroupCreatedEvent(group, context.getId())); - pageIndex += groupSize; } - savedRollout.setRolloutGroupsCreated(groupIndex); - savedRollout.setStatus(RolloutStatus.READY); + savedRollout.setRolloutGroupsCreated(amountOfGroups); return rolloutRepository.save(savedRollout); } + private Rollout createRolloutGroups(final List groupList, final RolloutGroupConditions conditions, + final Rollout rollout) { + RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING); + final JpaRollout savedRollout = (JpaRollout) rollout; + + // Preparing the groups + final List groups = groupList.stream().map( + group -> RolloutHelper.prepareRolloutGroupWithDefaultConditions(group, conditions)) + .collect(Collectors.toList()); + groups.forEach(RolloutHelper::verifyRolloutGroupHasConditions); + + verifyRolloutGroupTargetCounts(groups, savedRollout); + + // Persisting the groups + RolloutGroup lastSavedGroup = null; + for (final RolloutGroup srcGroup : groups) { + final JpaRolloutGroup group = new JpaRolloutGroup(); + group.setName(srcGroup.getName()); + group.setDescription(srcGroup.getDescription()); + group.setRollout(savedRollout); + group.setParent(lastSavedGroup); + group.setStatus(RolloutGroupStatus.CREATING); + + group.setTargetPercentage(srcGroup.getTargetPercentage()); + if (srcGroup.getTargetFilterQuery() != null) { + group.setTargetFilterQuery(srcGroup.getTargetFilterQuery()); + } else { + group.setTargetFilterQuery(""); + } + + group.setSuccessCondition(srcGroup.getSuccessCondition()); + group.setSuccessConditionExp(srcGroup.getSuccessConditionExp()); + + group.setSuccessAction(srcGroup.getSuccessAction()); + group.setSuccessActionExp(srcGroup.getSuccessActionExp()); + + group.setErrorCondition(srcGroup.getErrorCondition()); + group.setErrorConditionExp(srcGroup.getErrorConditionExp()); + + group.setErrorAction(srcGroup.getErrorAction()); + group.setErrorActionExp(srcGroup.getErrorActionExp()); + + lastSavedGroup = rolloutGroupRepository.save(group); + publishRolloutGroupCreatedEventAfterCommit(lastSavedGroup); + } + + savedRollout.setRolloutGroupsCreated(groups.size()); + return rolloutRepository.save(savedRollout); + } + + private void publishRolloutGroupCreatedEventAfterCommit(final RolloutGroup group) { + afterCommit + .afterCommit(() -> eventPublisher.publishEvent(new RolloutGroupCreatedEvent(group, context.getId()))); + } + + @Override + @Transactional(isolation = Isolation.READ_UNCOMMITTED) + @Modifying + public void fillRolloutGroupsWithTargets(final Rollout rollout) { + RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING); + final JpaRollout jpaRollout = (JpaRollout) rollout; + + List rolloutGroups = RolloutHelper.getOrderedGroups(rollout); + int readyGroups = 0; + int totalTargets = 0; + for (RolloutGroup group : rolloutGroups) { + if (group.getStatus() != RolloutGroupStatus.CREATING) { + readyGroups++; + totalTargets += group.getTotalTargets(); + continue; + } + + final RolloutGroup filledGroup = fillRolloutGroupWithTargets(rollout, group); + if(filledGroup.getStatus() == RolloutGroupStatus.READY) { + readyGroups++; + totalTargets += filledGroup.getTotalTargets(); + } + } + + // When all groups are ready the rollout status can be changed to be ready, too. + if(readyGroups == rolloutGroups.size()) { + jpaRollout.setStatus(RolloutStatus.READY); + jpaRollout.setLastCheck(0); + jpaRollout.setTotalTargets(totalTargets); + rolloutRepository.save(jpaRollout); + } + } + + private RolloutGroup fillRolloutGroupWithTargets(final Rollout rollout, final RolloutGroup group1) { + RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING); + + JpaRolloutGroup group = (JpaRolloutGroup) group1; + + final String baseFilter = RolloutHelper.getTargetFilterQuery(rollout); + final String groupTargetFilter; + if (StringUtils.isEmpty(group.getTargetFilterQuery())) { + groupTargetFilter = baseFilter; + } else { + groupTargetFilter = baseFilter + ";" + group.getTargetFilterQuery(); + } + + final List readyGroups = RolloutHelper.getGroupsByStatusIncludingGroup(rollout, + RolloutGroupStatus.READY, group); + + final long targetsInGroupFilter = targetManagement + .countAllTargetsByTargetFilterQueryAndNotInRolloutGroups(readyGroups, groupTargetFilter); + final long expectedInGroup = Math.round(group.getTargetPercentage() / 100 * (double) targetsInGroupFilter); + final long currentlyInGroup = rolloutTargetGroupRepository.countByRolloutGroup(group); + + // Switch the Group status to READY, when there are enough Targets in + // the Group + if (currentlyInGroup >= expectedInGroup) { + group.setStatus(RolloutGroupStatus.READY); + return rolloutGroupRepository.save(group); + } + + long targetsLeftToAdd = expectedInGroup - currentlyInGroup; + + try { + do { + // Add up to TRANSACTION_TARGETS of the left targets + // In case a TransactionException is thrown this loop aborts + targetsLeftToAdd -= assignTargetsToGroupInNewTransaction(rollout, group, groupTargetFilter, + Math.min(TRANSACTION_TARGETS, targetsLeftToAdd)); + } while (targetsLeftToAdd > 0); + + group.setStatus(RolloutGroupStatus.READY); + group.setTotalTargets(rolloutTargetGroupRepository.countByRolloutGroup(group).intValue()); + return rolloutGroupRepository.save(group); + + } catch (TransactionException e) { + LOGGER.warn("Transaction assigning Targets to RolloutGroup failed", e); + return group; + } + } + + private Long runInNewCountingTransaction(final String transactionName, TransactionCallback action) { + final DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + def.setName(transactionName); + def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + return new TransactionTemplate(txManager, def).execute(action); + } + + private long assignTargetsToGroupInNewTransaction(final Rollout rollout, final RolloutGroup group, + final String targetFilter, final long limit) { + + return runInNewCountingTransaction("assignTargetsToRolloutGroup", status -> { + final PageRequest pageRequest = new PageRequest(0, Math.toIntExact(limit)); + final List readyGroups = RolloutHelper.getGroupsByStatusIncludingGroup(rollout, + RolloutGroupStatus.READY, group); + Page targets = targetManagement.findAllTargetsByTargetFilterQueryAndNotInRolloutGroups(pageRequest, + readyGroups, targetFilter); + + createAssignmentOfTargetsToGroup(targets, group); + + return targets.getTotalElements(); + }); + } + + private void createAssignmentOfTargetsToGroup(final Page targets, final RolloutGroup group) { + targets.forEach(target -> rolloutTargetGroupRepository.save(new RolloutTargetGroup(group, target))); + } + + private void verifyRolloutGroupTargetCounts(final List groups, final JpaRollout rollout) { + final String baseFilter = RolloutHelper.getTargetFilterQuery(rollout); + final long totalTargets = targetManagement.countTargetByTargetFilterQuery(baseFilter); + if (totalTargets == 0) { + throw new RolloutVerificationException("Rollout target filter does not match any targets"); + } + + long targetCount = totalTargets; + long unusedTargetsCount = 0; + + for (int i = 0; i < groups.size(); i++) { + final RolloutGroup group = groups.get(i); + RolloutHelper.verifyRolloutGroupTargetPercentage(group.getTargetPercentage()); + + final long targetsInGroupFilter = countTargetsOfGroup(baseFilter, totalTargets, group); + final long overlappingTargets = countOverlappingTargetsWithPreviousGroups(baseFilter, groups, group, i); + + final long realTargetsInGroup; + // Assume that targets which were not used in the previous groups are used in this group + if (overlappingTargets > 0 && unusedTargetsCount > 0) { + realTargetsInGroup = targetsInGroupFilter - overlappingTargets + unusedTargetsCount; + unusedTargetsCount = 0; + } else { + realTargetsInGroup = targetsInGroupFilter - overlappingTargets; + } + + final long reducedTargetsInGroup = Math + .round(group.getTargetPercentage() / 100 * (double) realTargetsInGroup); + targetCount -= reducedTargetsInGroup; + unusedTargetsCount += realTargetsInGroup - reducedTargetsInGroup; + + } + + RolloutHelper.verifyRemainingTargets(targetCount); + + } + + private long countTargetsOfGroup(final String baseFilter, final long baseFilterCount, final RolloutGroup group) { + if (StringUtils.isEmpty(group.getTargetFilterQuery())) { + return baseFilterCount; + } else { + return targetManagement + .countTargetByTargetFilterQuery(baseFilter + ";" + group.getTargetFilterQuery()); + } + } + + private long countOverlappingTargetsWithPreviousGroups(final String baseFilter, final List groups, + final RolloutGroup group, final int groupIndex) { + // there can't be overlapping targets in the first group + if(groupIndex == 0) { + return 0; + } + final List previousGroups = groups.subList(0, groupIndex); + String overlappingTargetsFilter = RolloutHelper.getOverlappingWithGroupsTargetFilter(previousGroups, group); + if (StringUtils.isEmpty(overlappingTargetsFilter)) { + overlappingTargetsFilter = baseFilter; + } else { + overlappingTargetsFilter = baseFilter + ";" + overlappingTargetsFilter; + } + return targetManagement.countTargetByTargetFilterQuery(overlappingTargetsFilter); + } + @Override @Transactional(isolation = Isolation.READ_UNCOMMITTED) @Modifying public Rollout startRollout(final Rollout rollout) { - final JpaRollout mergedRollout = entityManager.merge((JpaRollout) rollout); - checkIfRolloutCanStarted(rollout, mergedRollout); - return doStartRollout(mergedRollout); - } - - @Override - @Transactional(isolation = Isolation.READ_UNCOMMITTED) - @Modifying - public Rollout startRolloutAsync(final Rollout rollout) { final JpaRollout mergedRollout = entityManager.merge((JpaRollout) rollout); checkIfRolloutCanStarted(rollout, mergedRollout); mergedRollout.setStatus(RolloutStatus.STARTING); - final JpaRollout updatedRollout = rolloutRepository.save(mergedRollout); - startingRollouts.add(updatedRollout.getName()); - executor.execute(() -> { - try { - final DefaultTransactionDefinition def = new DefaultTransactionDefinition(); - def.setName("startingRollout"); - def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - new TransactionTemplate(txManager, def).execute(status -> { - doStartRollout(updatedRollout); - return null; - }); - } finally { - startingRollouts.remove(updatedRollout.getName()); - } - }); - return updatedRollout; + return rolloutRepository.save(mergedRollout); + } + + private void startFirstRolloutGroup(final Rollout rollout) { + RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.STARTING); + final JpaRollout jpaRollout = (JpaRollout) rollout; + + final List rolloutGroups = rolloutGroupRepository.findByRolloutOrderByIdAsc(jpaRollout); + final JpaRolloutGroup rolloutGroup = rolloutGroups.get(0); + if (rolloutGroup.getParent() != null) { + throw new RolloutIllegalStateException("First Group is not the first group."); + } + + deploymentManagement.startScheduledActionsByRolloutGroupParent(rollout, null); + + rolloutGroup.setStatus(RolloutGroupStatus.RUNNING); + rolloutGroupRepository.save(rolloutGroup); + + jpaRollout.setStatus(RolloutStatus.RUNNING); + jpaRollout.setLastCheck(0); + rolloutRepository.save(jpaRollout); } - private Rollout doStartRollout(final JpaRollout rollout) { - final DistributionSet distributionSet = rollout.getDistributionSet(); - final ActionType actionType = rollout.getActionType(); - final long forceTime = rollout.getForcedTime(); - final List rolloutGroups = rolloutGroupRepository.findByRolloutOrderByIdAsc(rollout); - for (int iGroup = 0; iGroup < rolloutGroups.size(); iGroup++) { - final JpaRolloutGroup rolloutGroup = rolloutGroups.get(iGroup); - final List targetGroup = targetRepository.findByRolloutTargetGroupRolloutGroup(rolloutGroup); - // firstgroup can already be started - if (iGroup == 0) { - final List targetsWithActionType = targetGroup.stream() - .map(t -> new TargetWithActionType(t.getControllerId(), actionType, forceTime)) - .collect(Collectors.toList()); - deploymentManagement.assignDistributionSet(distributionSet.getId(), targetsWithActionType, rollout, - rolloutGroup); - rolloutGroup.setStatus(RolloutGroupStatus.RUNNING); - } else { - // create only not active actions with status scheduled so they - // can be activated later - deploymentManagement.createScheduledAction(targetGroup, distributionSet, actionType, forceTime, rollout, - rolloutGroup); - rolloutGroup.setStatus(RolloutGroupStatus.SCHEDULED); - } - rolloutGroupRepository.save(rolloutGroup); + private boolean ensureAllGroupsAreScheduled(final Rollout rollout) { + RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.STARTING); + final JpaRollout jpaRollout = (JpaRollout) rollout; + + final List groupsToBeScheduled = rolloutGroupRepository.findByRolloutAndStatus(rollout, + RolloutGroupStatus.READY); + final long scheduledGroups = groupsToBeScheduled.stream() + .filter(group -> scheduleRolloutGroup(jpaRollout, group)).count(); + + return scheduledGroups == groupsToBeScheduled.size(); + } + + /** + * Schedules a group of the rollout. Scheduled Actions are created to + * achieve this. The creation of those Actions is allowed to fail. + * + * @param rollout + * the Rollout + * @param group + * the RolloutGroup + * @return whether the complete group was scheduled + */ + private boolean scheduleRolloutGroup(final JpaRollout rollout, final JpaRolloutGroup group) { + final long targetsInGroup = rolloutTargetGroupRepository.countByRolloutGroup(group); + final long countOfActions = actionRepository.countByRolloutAndRolloutGroup(rollout, group); + + long actionsLeft = targetsInGroup - countOfActions; + if (actionsLeft > 0) { + actionsLeft -= createActionsForRolloutGroup(rollout, group); } - rollout.setStatus(RolloutStatus.RUNNING); - return rolloutRepository.save(rollout); + + if (actionsLeft <= 0) { + group.setStatus(RolloutGroupStatus.SCHEDULED); + rolloutGroupRepository.save(group); + return true; + } + return false; + } + + private long createActionsForRolloutGroup(final Rollout rollout, final RolloutGroup group) { + long totalActionsCreated = 0; + try { + long actionsCreated; + do { + actionsCreated = createActionsForTargetsInNewTransaction(rollout.getId(), group.getId(), + TRANSACTION_TARGETS); + totalActionsCreated += actionsCreated; + } while (actionsCreated > 0); + + } catch (TransactionException e) { + LOGGER.warn("Transaction assigning Targets to RolloutGroup failed", e); + return 0; + } + return totalActionsCreated; + } + + private long createActionsForTargetsInNewTransaction(final long rolloutId, final long groupId, final long limit) { + return runInNewCountingTransaction("createActionsForTargets", status -> { + final PageRequest pageRequest = new PageRequest(0, Math.toIntExact(limit)); + final Rollout rollout = rolloutRepository.findOne(rolloutId); + final RolloutGroup group = rolloutGroupRepository.findOne(groupId); + + final DistributionSet distributionSet = rollout.getDistributionSet(); + final ActionType actionType = rollout.getActionType(); + final long forceTime = rollout.getForcedTime(); + + Page targets = targetManagement.findAllTargetsInRolloutGroupWithoutAction(pageRequest, group); + if (targets.getTotalElements() > 0) { + deploymentManagement.createScheduledAction(targets.getContent(), distributionSet, actionType, forceTime, + rollout, group); + } + + return targets.getTotalElements(); + }); } @Override @@ -384,7 +598,6 @@ public class JpaRolloutManagement implements RolloutManagement { @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED) @Modifying public void checkRunningRollouts(final long delayBetweenChecks) { - verifyStuckedRollouts(); final long lastCheck = System.currentTimeMillis(); final int updated = rolloutRepository.updateLastCheck(lastCheck, delayBetweenChecks, RolloutStatus.RUNNING); @@ -425,36 +638,6 @@ public class JpaRolloutManagement implements RolloutManagement { } } - /** - * Verifies and handles stucked rollouts in asynchronous creation or - * starting state. If rollouts are created or started asynchronously it - * might be that they keep in state {@link RolloutStatus#CREATING} or - * {@link RolloutStatus#STARTING} due database or application interruption. - * In case this happens, set the rollout to error state. - */ - private void verifyStuckedRollouts() { - final List rolloutsInCreatingState = rolloutRepository.findByStatus(RolloutStatus.CREATING); - rolloutsInCreatingState.stream().filter(rollout -> !creatingRollouts.contains(rollout.getName())) - .forEach(rollout -> { - LOGGER.warn( - "Determined error during rollout creation of rollout {}, stucking in creating state, setting to status", - rollout, RolloutStatus.ERROR_CREATING); - rollout.setStatus(RolloutStatus.ERROR_CREATING); - rolloutRepository.save(rollout); - }); - - final List rolloutsInStartingState = rolloutRepository.findByStatus(RolloutStatus.STARTING); - rolloutsInStartingState.stream().filter(rollout -> !startingRollouts.contains(rollout.getName())) - .forEach(rollout -> { - LOGGER.warn( - "Determined error during rollout starting of rollout {}, stucking in starting state, setting to status", - rollout, RolloutStatus.ERROR_STARTING); - rollout.setStatus(RolloutStatus.ERROR_STARTING); - rolloutRepository.save(rollout); - }); - - } - private void executeRolloutGroups(final JpaRollout rollout, final List rolloutGroups) { for (final JpaRolloutGroup rolloutGroup : rolloutGroups) { @@ -573,6 +756,54 @@ public class JpaRolloutManagement implements RolloutManagement { rolloutGroup, rolloutGroup.getSuccessActionExp()); } + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED) + @Modifying + public void checkCreatingRollouts(long delayBetweenChecks) { + final long lastCheck = System.currentTimeMillis(); + final int updated = rolloutRepository.updateLastCheck(lastCheck, delayBetweenChecks, RolloutStatus.CREATING); + if (updated == 0) { + // nothing to check, maybe another instance already checked in + // between + LOGGER.debug("No rollouts creating check necessary for current scheduled check {}, next check at {}", lastCheck, + lastCheck + delayBetweenChecks); + return; + } + + final List rolloutsToCheck = rolloutRepository.findByLastCheckAndStatus(lastCheck, + RolloutStatus.CREATING); + LOGGER.info("Found {} creating rollouts to check", rolloutsToCheck.size()); + + rolloutsToCheck.forEach(this::fillRolloutGroupsWithTargets); + + } + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED) + @Modifying + public void checkStartingRollouts(long delayBetweenChecks) { + final long lastCheck = System.currentTimeMillis(); + final int updated = rolloutRepository.updateLastCheck(lastCheck, delayBetweenChecks, RolloutStatus.STARTING); + if (updated == 0) { + // nothing to check, maybe another instance already checked in + // between + LOGGER.debug("No rollouts starting check necessary for current scheduled check {}, next check at {}", lastCheck, + lastCheck + delayBetweenChecks); + return; + } + + final List rolloutsToCheck = rolloutRepository.findByLastCheckAndStatus(lastCheck, + RolloutStatus.STARTING); + LOGGER.info("Found {} starting rollouts to check", rolloutsToCheck.size()); + + rolloutsToCheck.forEach(rollout -> { + if(ensureAllGroupsAreScheduled(rollout)) { + startFirstRolloutGroup(rollout); + } + }); + + } + @Override public Long countRolloutsAll() { return rolloutRepository.count(); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java index b42e33b3c..e8932927e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java @@ -43,6 +43,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTargetInfo_; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; +import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications; @@ -585,6 +586,38 @@ public class JpaTargetManagement implements TargetManagement { } + @Override + public Page findAllTargetsByTargetFilterQueryAndNotInRolloutGroups(@NotNull final Pageable pageRequest, + final List groups, @NotNull final String targetFilterQuery) { + + final Specification spec = RSQLUtility.parse(targetFilterQuery, TargetFields.class, + virtualPropertyReplacer); + + return findTargetsBySpec((root, cq, cb) -> cb.and(spec.toPredicate(root, cq, cb), + TargetSpecifications.isNotInRolloutGroups(groups).toPredicate(root, cq, cb)), pageRequest); + + } + + @Override + public Page findAllTargetsInRolloutGroupWithoutAction(@NotNull final Pageable pageRequest, + @NotNull final RolloutGroup group) { + return findTargetsBySpec( + (root, cq, cb) -> TargetSpecifications.hasNoActionInRolloutGroup(group).toPredicate(root, cq, cb), + pageRequest); + } + + @Override + public Long countAllTargetsByTargetFilterQueryAndNotInRolloutGroups(final List groups, + @NotNull final String targetFilterQuery) { + final Specification spec = RSQLUtility.parse(targetFilterQuery, TargetFields.class, + virtualPropertyReplacer); + final List> specList = new ArrayList<>(2); + specList.add(spec); + specList.add(TargetSpecifications.isNotInRolloutGroups(groups)); + + return countByCriteriaAPI(specList); + } + @Override public Long countTargetsByTargetFilterQueryAndNonDS(final Long distributionSetId, @NotNull final TargetFilterQuery targetFilterQuery) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutHelper.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutHelper.java new file mode 100644 index 000000000..f7720f86a --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutHelper.java @@ -0,0 +1,246 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations 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.repository.jpa; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; +import org.eclipse.hawkbit.repository.exception.RolloutVerificationException; +import org.eclipse.hawkbit.repository.model.Rollout; +import org.eclipse.hawkbit.repository.model.RolloutGroup; +import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * A collection of static helper methods for the {@link JpaRolloutManagement} + */ +final class RolloutHelper { + private RolloutHelper() { + } + + /** + * Verifies that the required success condition and action are actually set. + * + * @param conditions + * input conditions and actions + */ + static void verifyRolloutGroupConditions(final RolloutGroupConditions conditions) { + if (conditions.getSuccessCondition() == null) { + throw new RolloutVerificationException("Rollout group is missing success condition"); + } + if (conditions.getSuccessAction() == null) { + throw new RolloutVerificationException("Rollout group is missing success action"); + } + } + + /** + * Verifies that the group has the required success condition and action. + * + * @param group + * the input group + * @return the verified group + */ + static RolloutGroup verifyRolloutGroupHasConditions(final RolloutGroup group) { + if (group.getSuccessCondition() == null) { + throw new RolloutVerificationException("Rollout group is missing success condition"); + } + if (group.getSuccessAction() == null) { + throw new RolloutVerificationException("Rollout group is missing success action"); + } + return group; + } + + /** + * In case the given group is missing conditions or actions, they will be + * set from the supplied default conditions. + * + * @param group + * group to check + * @param conditions + * default conditions and actions + * @return group with all conditions and actions + */ + static RolloutGroup prepareRolloutGroupWithDefaultConditions(final RolloutGroup group, + final RolloutGroupConditions conditions) { + if (group.getSuccessCondition() == null) { + group.setSuccessCondition(conditions.getSuccessCondition()); + } + if (group.getSuccessConditionExp() == null) { + group.setSuccessConditionExp(conditions.getSuccessConditionExp()); + } + if (group.getSuccessAction() == null) { + group.setSuccessAction(conditions.getSuccessAction()); + } + if (group.getSuccessActionExp() == null) { + group.setSuccessActionExp(conditions.getSuccessActionExp()); + } + + if (group.getErrorCondition() == null) { + group.setErrorCondition(conditions.getErrorCondition()); + } + if (group.getErrorConditionExp() == null) { + group.setErrorConditionExp(conditions.getErrorConditionExp()); + } + if (group.getErrorAction() == null) { + group.setErrorAction(conditions.getErrorAction()); + } + if (group.getErrorActionExp() == null) { + group.setErrorActionExp(conditions.getErrorActionExp()); + } + return group; + } + + /** + * Verify if the supplied amount of groups is in range + * + * @param amountGroup + * amount of groups + */ + static void verifyRolloutGroupParameter(final int amountGroup) { + if (amountGroup <= 0) { + throw new RolloutVerificationException("the amountGroup must be greater than zero"); + } else if (amountGroup > 500) { + throw new RolloutVerificationException("the amountGroup must not be greater than 500"); + } + } + + /** + * Verify that the supplied percentage is in range + * + * @param percentage + * the percentage + */ + static void verifyRolloutGroupTargetPercentage(final float percentage) { + if (percentage <= 0) { + throw new RolloutVerificationException("the percentage must be greater than zero"); + } else if (percentage > 100) { + throw new RolloutVerificationException("the percentage must not be greater than 100"); + } + } + + /** + * Modifies the target filter query to only match targets that were created + * after the Rollout. + * + * @param rollout + * Rollout to derive the filter from + * @return resulting target filter query + */ + static String getTargetFilterQuery(final Rollout rollout) { + if (rollout.getCreatedAt() != null) { + return rollout.getTargetFilterQuery() + ";createdat=le=" + rollout.getCreatedAt().toString(); + } + return rollout.getTargetFilterQuery(); + } + + /** + * Verifies that the Rollout is in the required status. + * + * @param rollout + * the Rollout + * @param status + * the Status + */ + static void verifyRolloutInStatus(final Rollout rollout, final Rollout.RolloutStatus status) { + if (!rollout.getStatus().equals(status)) { + throw new RolloutIllegalStateException("Rollout is not in status " + status.toString()); + } + } + + /** + * Filters the groups of a Rollout to match a specific status and adds a + * group to the result. + * + * @param rollout + * the rollout + * @param status + * the required status for the groups + * @param group + * the group to add + * @return list of groups + */ + static List getGroupsByStatusIncludingGroup(final Rollout rollout, + final RolloutGroup.RolloutGroupStatus status, final RolloutGroup group) { + return rollout.getRolloutGroups().stream() + .filter(innerGroup -> innerGroup.getStatus().equals(status) || innerGroup.equals(group)) + .collect(Collectors.toList()); + } + + /** + * Returns the groups of a rollout by their Ids order + * + * @param rollout + * the rollout + * @return ordered list of groups + */ + static List getOrderedGroups(final Rollout rollout) { + return rollout.getRolloutGroups().stream().sorted((group1, group2) -> { + if (group1.getId() < group2.getId()) { + return -1; + } + if (group1.getId() > group2.getId()) { + return 1; + } + return 0; + }).collect(Collectors.toList()); + } + + /** + * Creates an RSQL expression that matches all targets in the provided groups. + * Links all target filter queries with OR. + * + * @param groups the rollout groups + * @return RSQL string without base filter of the Rollout. Can be an empty string. + */ + static String getAllGroupsTargetFilter(final List groups) { + if (groups.stream().anyMatch(group -> StringUtils.isEmpty(group.getTargetFilterQuery()))) { + return ""; + } + return groups.stream().map(RolloutGroup::getTargetFilterQuery).collect(Collectors.joining(",")); + } + + /** + * Creates an RSQL Filter that matches all targets that are in the provided group and + * in the provided groups. + * + * @param groups the rollout groups + * @param group the group + * @return RSQL string without base filter of the Rollout. Can be an empty string. + */ + static String getOverlappingWithGroupsTargetFilter(final List groups, final RolloutGroup group) { + final String previousGroupFilters = getAllGroupsTargetFilter(groups); + if (StringUtils.isNotEmpty(previousGroupFilters) && StringUtils.isNotEmpty(group.getTargetFilterQuery())) { + return group.getTargetFilterQuery() + ";(" + previousGroupFilters + ")"; + } else if (StringUtils.isNotEmpty(previousGroupFilters)) { + return "(" + previousGroupFilters + ")"; + } else if (StringUtils.isNotEmpty(group.getTargetFilterQuery())) { + return group.getTargetFilterQuery(); + } else { + return ""; + } + } + + /** + * Verifies that no targets are left + * + * @param targetCount + * the count of left targets + */ + static void verifyRemainingTargets(final long targetCount) { + if (targetCount > 0) { + throw new RolloutVerificationException( + "Rollout groups don't match all targets that are targeted by the rollout"); + } + if (targetCount != 0) { + throw new RolloutVerificationException("Rollout groups target count verification failed"); + } + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutTargetGroupRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutTargetGroupRepository.java index 8cb267c69..a4fc68541 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutTargetGroupRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutTargetGroupRepository.java @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.repository.jpa; +import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; import org.eclipse.hawkbit.repository.jpa.model.RolloutTargetGroup; import org.eclipse.hawkbit.repository.jpa.model.RolloutTargetGroupId; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -22,4 +23,13 @@ import org.springframework.transaction.annotation.Transactional; @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) public interface RolloutTargetGroupRepository extends CrudRepository, JpaSpecificationExecutor { + + /** + * Counts all entries that have the specified rolloutGroup + * + * @param rolloutGroup + * the group to filter for + * @return count of targets in the group + */ + Long countByRolloutGroup(final JpaRolloutGroup rolloutGroup); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRolloutGroup.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRolloutGroup.java index 31f282a64..dbf70b86e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRolloutGroup.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRolloutGroup.java @@ -53,7 +53,7 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr private JpaRollout rollout; @Column(name = "status") - private RolloutGroupStatus status = RolloutGroupStatus.READY; + private RolloutGroupStatus status = RolloutGroupStatus.CREATING; @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST }, targetEntity = RolloutTargetGroup.class) @JoinColumn(name = "rolloutGroup_Id", insertable = false, updatable = false) @@ -93,6 +93,13 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr @Column(name = "total_targets") private int totalTargets; + @Column(name = "target_filter", length = 1024) + @Size(max = 1024) + private String targetFilterQuery = ""; + + @Column(name = "target_percentage") + private float targetPercentage = 100; + @Transient private transient TotalTargetCountStatus totalTargetCountStatus; @@ -212,14 +219,36 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr this.totalTargets = totalTargets; } + @Override public void setSuccessAction(final RolloutGroupSuccessAction successAction) { this.successAction = successAction; } + @Override public void setSuccessActionExp(final String successActionExp) { this.successActionExp = successActionExp; } + @Override + public String getTargetFilterQuery() { + return targetFilterQuery; + } + + @Override + public void setTargetFilterQuery(String targetFilterQuery) { + this.targetFilterQuery = targetFilterQuery; + } + + @Override + public float getTargetPercentage() { + return targetPercentage; + } + + @Override + public void setTargetPercentage(float targetPercentage) { + this.targetPercentage = targetPercentage; + } + /** * @return the totalTargetCountStatus */ @@ -242,10 +271,10 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr @Override public String toString() { - return "RolloutGroup [rollout=" + rollout + ", status=" + status + ", rolloutTargetGroup=" + rolloutTargetGroup - + ", parent=" + parent + ", finishCondition=" + successCondition + ", finishExp=" + successConditionExp - + ", errorCondition=" + errorCondition + ", errorExp=" + errorConditionExp + ", getName()=" + getName() - + ", getId()=" + getId() + "]"; + return "RolloutGroup [rollout=" + (rollout != null ? rollout.getId() : "") + ", status=" + status + + ", rolloutTargetGroup=" + rolloutTargetGroup + ", parent=" + parent + ", finishCondition=" + + successCondition + ", finishExp=" + successConditionExp + ", errorCondition=" + errorCondition + + ", errorExp=" + errorConditionExp + ", getName()=" + getName() + ", getId()=" + getId() + "]"; } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/RolloutScheduler.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/RolloutScheduler.java index 7335c1cb6..b02bb65bc 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/RolloutScheduler.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/RolloutScheduler.java @@ -73,7 +73,10 @@ public class RolloutScheduler { LOGGER.info("Checking rollouts for {} tenants", tenants.size()); for (final String tenant : tenants) { tenantAware.runAsTenant(tenant, () -> { - rolloutManagement.checkRunningRollouts(rolloutProperties.getScheduler().getFixedDelay()); + final long fixedDelay = rolloutProperties.getScheduler().getFixedDelay(); + rolloutManagement.checkCreatingRollouts(fixedDelay); + rolloutManagement.checkStartingRollouts(fixedDelay); + rolloutManagement.checkRunningRollouts(fixedDelay); return null; }); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/StartNextGroupRolloutGroupSuccessAction.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/StartNextGroupRolloutGroupSuccessAction.java index 958613f58..8543b8207 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/StartNextGroupRolloutGroupSuccessAction.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/StartNextGroupRolloutGroupSuccessAction.java @@ -13,7 +13,6 @@ import java.util.List; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.jpa.RolloutGroupRepository; import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; -import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus; @@ -57,14 +56,11 @@ public class StartNextGroupRolloutGroupSuccessAction implements RolloutGroupActi // retrieve all actions according to the parent group of the finished // rolloutGroup, so retrieve all child-group actions which need to be // started. - final List rolloutGroupActions = deploymentManagement.findActionsByRolloutGroupParentAndStatus(rollout, - rolloutGroup, Action.Status.SCHEDULED); - logger.debug("{} Next actions to start for rollout {} and parent group {}", rolloutGroupActions.size(), rollout, + final long countOfStartedActions = deploymentManagement.startScheduledActionsByRolloutGroupParent(rollout, rolloutGroup); - rolloutGroupActions.forEach(deploymentManagement::startScheduledAction); - logger.debug("{} actions started for rollout {} and parent group {}", rolloutGroupActions.size(), rollout, + logger.debug("{} Next actions started for rollout {} and parent group {}", countOfStartedActions, rollout, rolloutGroup); - if (!rolloutGroupActions.isEmpty()) { + if (countOfStartedActions > 0) { // get all next scheduled groups and set them in state running rolloutGroupRepository.setStatusForCildren(RolloutGroupStatus.RUNNING, rolloutGroup); } else { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java index 99112cd38..344ab4951 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java @@ -31,7 +31,10 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTargetInfo_; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag_; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; +import org.eclipse.hawkbit.repository.jpa.model.RolloutTargetGroup; +import org.eclipse.hawkbit.repository.jpa.model.RolloutTargetGroup_; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetInfo; import org.eclipse.hawkbit.repository.model.TargetTag; @@ -255,6 +258,45 @@ public final class TargetSpecifications { }; } + /** + * {@link Specification} for retrieving {@link Target}s that are not in the + * given {@link RolloutGroup}s + * + * @param groups + * the {@link RolloutGroup}s + * @return the {@link Target} {@link Specification} + */ + public static Specification isNotInRolloutGroups(final List groups) { + return (targetRoot, query, cb) -> { + ListJoin rolloutTargetJoin = targetRoot.join(JpaTarget_.rolloutTargetGroup, + JoinType.LEFT); + Predicate inRolloutGroups = rolloutTargetJoin.get(RolloutTargetGroup_.rolloutGroup).in(groups); + rolloutTargetJoin.on(inRolloutGroups); + return cb.isNull(rolloutTargetJoin.get(RolloutTargetGroup_.target)); + }; + } + + /** + * {@link Specification} for retrieving {@link Target}s that have no Action + * of the {@link RolloutGroup}. + * + * @param group + * the {@link RolloutGroup} + * @return the {@link Target} {@link Specification} + */ + public static Specification hasNoActionInRolloutGroup(final RolloutGroup group) { + return (targetRoot, query, cb) -> { + ListJoin rolloutTargetJoin = targetRoot.join(JpaTarget_.rolloutTargetGroup, + JoinType.INNER); + rolloutTargetJoin.on(cb.equal(rolloutTargetJoin.get(RolloutTargetGroup_.rolloutGroup), group)); + + final ListJoin actionsJoin = targetRoot.join(JpaTarget_.actions, JoinType.LEFT); + actionsJoin.on(cb.equal(actionsJoin.get(JpaAction_.rolloutGroup), group)); + + return cb.isNull(actionsJoin.get(JpaAction_.id)); + }; + } + /** * {@link Specification} for retrieving {@link Target}s by assigned * distribution set. diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_10_0__advanced_rolloutgroup__H2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_10_0__advanced_rolloutgroup__H2.sql new file mode 100644 index 000000000..7c4954ad8 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_10_0__advanced_rolloutgroup__H2.sql @@ -0,0 +1,4 @@ +ALTER TABLE sp_rolloutgroup + ADD COLUMN target_percentage FLOAT; +ALTER TABLE sp_rolloutgroup + ADD COLUMN target_filter VARCHAR (1024); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_10_0__advanced_rolloutgroup__MYSQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_10_0__advanced_rolloutgroup__MYSQL.sql new file mode 100644 index 000000000..7c4954ad8 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_10_0__advanced_rolloutgroup__MYSQL.sql @@ -0,0 +1,4 @@ +ALTER TABLE sp_rolloutgroup + ADD COLUMN target_percentage FLOAT; +ALTER TABLE sp_rolloutgroup + ADD COLUMN target_filter VARCHAR (1024); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java index ce1fe0b9a..aada8a6eb 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java @@ -9,16 +9,21 @@ package org.eclipse.hawkbit.repository.jpa; import static org.fest.assertions.api.Assertions.assertThat; +import static org.fest.assertions.api.Assertions.fail; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; +import org.eclipse.hawkbit.repository.exception.RolloutVerificationException; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus; import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; @@ -190,7 +195,11 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { final Rollout createdRollout = createSimpleTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout, amountOtherTargets, amountGroups, successCondition, errorCondition); rolloutManagement.startRollout(createdRollout); - return createdRollout; + + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + + return rolloutManagement.findRolloutById(createdRollout.getId()); } @Step("Finish three actions of the rollout group and delete two targets") @@ -407,6 +416,10 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { validateRolloutActionStatus(createdRollout.getId(), validationMap); rolloutManagement.startRollout(createdRollout); + + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + // 6 targets are ready and 2 are running validationMap = createInitStatusMap(); validationMap.put(TotalTargetCountStatus.Status.SCHEDULED, 6L); @@ -466,6 +479,10 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { validateRolloutActionStatus(createdRollout.getId(), validationMap); rolloutManagement.startRollout(createdRollout); + + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + // 6 targets are ready and 2 are running validationMap = createInitStatusMap(); validationMap.put(TotalTargetCountStatus.Status.SCHEDULED, 6L); @@ -496,19 +513,28 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { changeStatusForAllRunningActions(createdRollout, Status.FINISHED); rolloutManagement.checkRunningRollouts(0); - // 3 targets finished (Group 1), 3 targets running (Group 3) and 3 - // targets SCHEDULED (Group 3) + // round(9/4)=2 targets finished (Group 1) + // round(7/3)=2 targets running (Group 3) + // round(5/2)=3 targets SCHEDULED (Group 3) + // round(2/1)=2 targets SCHEDULED (Group 4) createdRollout = rolloutManagement.findRolloutById(createdRollout.getId()); - final List rolloutGruops = createdRollout.getRolloutGroups(); + final List rolloutGroups = createdRollout.getRolloutGroups(); + Map expectedTargetCountStatus = createInitStatusMap(); - expectedTargetCountStatus.put(TotalTargetCountStatus.Status.FINISHED, 3L); - validateRolloutGroupActionStatus(rolloutGruops.get(0), expectedTargetCountStatus); + expectedTargetCountStatus.put(TotalTargetCountStatus.Status.FINISHED, 2L); + validateRolloutGroupActionStatus(rolloutGroups.get(0), expectedTargetCountStatus); + expectedTargetCountStatus = createInitStatusMap(); - expectedTargetCountStatus.put(TotalTargetCountStatus.Status.RUNNING, 3L); - validateRolloutGroupActionStatus(rolloutGruops.get(1), expectedTargetCountStatus); + expectedTargetCountStatus.put(TotalTargetCountStatus.Status.RUNNING, 2L); + validateRolloutGroupActionStatus(rolloutGroups.get(1), expectedTargetCountStatus); + expectedTargetCountStatus = createInitStatusMap(); expectedTargetCountStatus.put(TotalTargetCountStatus.Status.SCHEDULED, 3L); - validateRolloutGroupActionStatus(rolloutGruops.get(2), expectedTargetCountStatus); + validateRolloutGroupActionStatus(rolloutGroups.get(2), expectedTargetCountStatus); + + expectedTargetCountStatus = createInitStatusMap(); + expectedTargetCountStatus.put(TotalTargetCountStatus.Status.SCHEDULED, 2L); + validateRolloutGroupActionStatus(rolloutGroups.get(3), expectedTargetCountStatus); } @@ -584,6 +610,10 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { validateRolloutActionStatus(rolloutOne.getId(), expectedTargetCountStatus); rolloutManagement.startRollout(rolloutTwo); + + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + // Verify that 5 targets are finished, 5 are still running and 5 are // cancelled. expectedTargetCountStatus = createInitStatusMap(); @@ -631,11 +661,15 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { amountGroupsForRolloutTwo, "controllerId==rollout-*", distributionSet, "50", "80"); rolloutManagement.startRollout(rolloutTwo); + + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + rolloutTwo = rolloutManagement.findRolloutById(rolloutTwo.getId()); // 6 error targets are know running expectedTargetCountStatus = createInitStatusMap(); expectedTargetCountStatus.put(TotalTargetCountStatus.Status.RUNNING, 6L); - expectedTargetCountStatus.put(TotalTargetCountStatus.Status.NOTSTARTED, 9L); + expectedTargetCountStatus.put(TotalTargetCountStatus.Status.FINISHED, 9L); validateRolloutActionStatus(rolloutTwo.getId(), expectedTargetCountStatus); changeStatusForAllRunningActions(rolloutTwo, Status.FINISHED); final Page targetPage = targetManagement.findTargetByUpdateStatus(pageReq, TargetUpdateStatus.IN_SYNC); @@ -729,12 +763,15 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { final Rollout rolloutA = createTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout, amountGroups, successCondition, errorCondition, "RolloutA", "RolloutA"); rolloutManagement.startRollout(rolloutA); + rolloutManagement.checkStartingRollouts(0); final int amountTargetsForRollout2 = 10; final int amountGroups2 = 2; final Rollout rolloutB = createTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout2, amountGroups2, successCondition, errorCondition, "RolloutB", "RolloutB"); rolloutManagement.startRollout(rolloutB); + rolloutManagement.checkStartingRollouts(0); + changeStatusForAllRunningActions(rolloutB, Status.FINISHED); rolloutManagement.checkRunningRollouts(0); @@ -743,6 +780,8 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { final Rollout rolloutC = createTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout3, amountGroups3, successCondition, errorCondition, "RolloutC", "RolloutC"); rolloutManagement.startRollout(rolloutC); + rolloutManagement.checkStartingRollouts(0); + changeStatusForAllRunningActions(rolloutC, Status.ERROR); rolloutManagement.checkRunningRollouts(0); @@ -751,6 +790,8 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { final Rollout rolloutD = createTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout4, amountGroups4, successCondition, errorCondition, "RolloutD", "RolloutD"); rolloutManagement.startRollout(rolloutD); + rolloutManagement.checkStartingRollouts(0); + changeStatusForRunningActions(rolloutD, Status.ERROR, 1); rolloutManagement.checkRunningRollouts(0); changeStatusForAllRunningActions(rolloutD, Status.FINISHED); @@ -882,6 +923,10 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { Rollout myRollout = createTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout, amountGroups, successCondition, errorCondition, rolloutName, rolloutName); rolloutManagement.startRollout(myRollout); + + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + changeStatusForRunningActions(myRollout, Status.FINISHED, 2); rolloutManagement.checkRunningRollouts(0); myRollout = rolloutManagement.findRolloutById(myRollout.getId()); @@ -924,6 +969,10 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { final String rsqlParam = "controllerId==*MyRoll*"; rolloutManagement.startRollout(myRollout); + + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + myRollout = rolloutManagement.findRolloutById(myRollout.getId()); final List rolloutGroups = myRollout.getRolloutGroups(); @@ -948,8 +997,101 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Verify the creation and the start of a rollout in asynchronous mode.") - public void createAndStartRolloutInAsync() throws Exception { + @Description("Verify the creation of a Rollout without targets throws an Exception.") + public void createRolloutNotMatchingTargets() { + final int amountGroups = 5; + final String successCondition = "50"; + final String errorCondition = "80"; + final String rolloutName = "rolloutTest3"; + + final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName); + + try { + createRolloutByVariables(rolloutName, "desc", amountGroups, "id==notExisting", distributionSet, + successCondition, errorCondition); + fail("Was able to create a Rollout without targets."); + } catch(RolloutVerificationException e) { + // OK + } + + } + + @Test + @Description("Verify the creation of a Rollout with the same name throws an Exception.") + public void createDuplicateRollout() { + final int amountGroups = 5; + final int amountTargetsForRollout = 10; + final String successCondition = "50"; + final String errorCondition = "80"; + final String rolloutName = "rolloutTest4"; + + targetManagement.createTargets(testdataFactory.generateTargets(amountTargetsForRollout, "dup-ro-", "rollout")); + + final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName); + createRolloutByVariables(rolloutName, "desc", amountGroups, "id==dup-ro-*", distributionSet, + successCondition, errorCondition); + + try { + createRolloutByVariables(rolloutName, "desc", amountGroups, "id==dup-ro-*", distributionSet, + successCondition, errorCondition); + fail("Was able to create a duplicate Rollout."); + } catch(EntityAlreadyExistsException e) { + // OK + } + + } + + @Test + @Description("Verify the creation and the start of a Rollout with more groups than targets.") + public void createAndStartRolloutWithEmptyGroups() throws Exception { + final int amountTargetsForRollout = 3; + final int amountGroups = 5; + final String successCondition = "50"; + final String errorCondition = "80"; + final String rolloutName = "rolloutTestG"; + final String targetPrefixName = rolloutName; + final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName); + targetManagement.createTargets( + testdataFactory.generateTargets(amountTargetsForRollout, targetPrefixName + "-", targetPrefixName)); + + Rollout myRollout = createRolloutByVariables(rolloutName, "desc", amountGroups, + "controllerId==" + targetPrefixName + "-*", distributionSet, successCondition, errorCondition); + + assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.READY); + + List groups = myRollout.getRolloutGroups(); + assertThat(groups.get(0).getStatus()).isEqualTo(RolloutGroupStatus.READY); + assertThat(groups.get(0).getTotalTargets()).isEqualTo(1); + assertThat(groups.get(1).getStatus()).isEqualTo(RolloutGroupStatus.READY); + assertThat(groups.get(1).getTotalTargets()).isEqualTo(1); + assertThat(groups.get(2).getStatus()).isEqualTo(RolloutGroupStatus.READY); + assertThat(groups.get(2).getTotalTargets()).isEqualTo(0); + assertThat(groups.get(3).getStatus()).isEqualTo(RolloutGroupStatus.READY); + assertThat(groups.get(3).getTotalTargets()).isEqualTo(1); + assertThat(groups.get(4).getStatus()).isEqualTo(RolloutGroupStatus.READY); + assertThat(groups.get(4).getTotalTargets()).isEqualTo(0); + + rolloutManagement.startRollout(myRollout); + + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + + SuccessConditionRolloutStatus conditionRolloutStatus = new SuccessConditionRolloutStatus(RolloutStatus.RUNNING); + assertThat(MultipleInvokeHelper.doWithTimeout(new RolloutStatusCallable(myRollout.getId()), + conditionRolloutStatus, 15000, 500)).as("Rollout status").isNotNull(); + + myRollout = rolloutManagement.findRolloutById(myRollout.getId()); + assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.RUNNING); + final Map expectedTargetCountStatus = createInitStatusMap(); + expectedTargetCountStatus.put(TotalTargetCountStatus.Status.RUNNING, 1L); + expectedTargetCountStatus.put(TotalTargetCountStatus.Status.SCHEDULED, 2L); + validateRolloutActionStatus(myRollout.getId(), expectedTargetCountStatus); + + } + + @Test + @Description("Verify the creation and the start of a rollout.") + public void createAndStartRollout() throws Exception { final int amountTargetsForRollout = 500; final int amountGroups = 5; @@ -960,28 +1102,18 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName); targetManagement.createTargets( testdataFactory.generateTargets(amountTargetsForRollout, targetPrefixName + "-", targetPrefixName)); - final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder() - .successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition) - .errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition) - .errorAction(RolloutGroupErrorAction.PAUSE, null).build(); - Rollout myRollout = new JpaRollout(); - myRollout.setName(rolloutName); - myRollout.setDescription("This is a test description for the rollout"); - myRollout.setTargetFilterQuery("controllerId==" + targetPrefixName + "-*"); - myRollout.setDistributionSet(distributionSet); - myRollout = rolloutManagement.createRolloutAsync(myRollout, amountGroups, conditions); + Rollout myRollout = createRolloutByVariables(rolloutName, "desc", amountGroups, + "controllerId==" + targetPrefixName + "-*", distributionSet, successCondition, errorCondition); + + assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.READY); + rolloutManagement.startRollout(myRollout); + + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); SuccessConditionRolloutStatus conditionRolloutTargetCount = new SuccessConditionRolloutStatus( - RolloutStatus.READY); - assertThat(MultipleInvokeHelper.doWithTimeout(new RolloutStatusCallable(myRollout.getId()), - conditionRolloutTargetCount, 15000, 500)).as("Rollout status").isNotNull(); - - myRollout = rolloutManagement.findRolloutById(myRollout.getId()); - assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.READY); - rolloutManagement.startRolloutAsync(myRollout); - - conditionRolloutTargetCount = new SuccessConditionRolloutStatus(RolloutStatus.RUNNING); + RolloutStatus.RUNNING); assertThat(MultipleInvokeHelper.doWithTimeout(new RolloutStatusCallable(myRollout.getId()), conditionRolloutTargetCount, 15000, 500)).as("Rollout status").isNotNull(); @@ -993,6 +1125,198 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { validateRolloutActionStatus(myRollout.getId(), expectedTargetCountStatus); } + @Test + @Description("Verify the creation of a Rollout with a groups definition.") + public void createRolloutWithGroupDefinition() throws Exception { + final String rolloutName = "rolloutTest3"; + + final int amountTargetsInGroup1 = 100; + final int percentTargetsInGroup1 = 100; + + final int amountTargetsInGroup1and2 = 500; + final int percentTargetsInGroup2 = 20; + final int percentTargetsInGroup3 = 100; + + final int countTargetsInGroup2 = (int) Math + .ceil((double) percentTargetsInGroup2 / 100 * (double) amountTargetsInGroup1and2); + final int countTargetsInGroup3 = amountTargetsInGroup1and2 - countTargetsInGroup2; + + final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().build(); + // Generate Targets for group 2 and 3 and generate the Rollout + Rollout myRollout = generateTargetsAndRollout(rolloutName, amountTargetsInGroup1and2); + + // Generate Targets for group 1 + targetManagement.createTargets( + testdataFactory.generateTargets(amountTargetsInGroup1, rolloutName + "-gr1-", rolloutName)); + + List rolloutGroups = new ArrayList<>(3); + rolloutGroups.add(generateRolloutGroup(0, percentTargetsInGroup1, "id==" + rolloutName + "-gr1-*")); + rolloutGroups.add(generateRolloutGroup(1, percentTargetsInGroup2, null)); + rolloutGroups.add(generateRolloutGroup(2, percentTargetsInGroup3, null)); + + myRollout = rolloutManagement.createRollout(myRollout, rolloutGroups, conditions); + myRollout = rolloutManagement.findRolloutById(myRollout.getId()); + + assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.CREATING); + for (RolloutGroup group : myRollout.getRolloutGroups()) { + assertThat(group.getStatus()).isEqualTo(RolloutGroupStatus.CREATING); + } + + // Generate Targets that must not be addressed by the rollout, because + // they were added after the rollout was created + TimeUnit.SECONDS.sleep(1); + targetManagement.createTargets(testdataFactory.generateTargets(10, rolloutName + "-notIn-", rolloutName)); + + rolloutManagement.fillRolloutGroupsWithTargets(myRollout); + + myRollout = rolloutManagement.findRolloutById(myRollout.getId()); + assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.READY); + assertThat(myRollout.getTotalTargets()).isEqualTo(amountTargetsInGroup1and2 + amountTargetsInGroup1); + + List groups = myRollout.getRolloutGroups(); + assertThat(groups.get(0).getStatus()).isEqualTo(RolloutGroupStatus.READY); + assertThat(groups.get(0).getTotalTargets()).isEqualTo(amountTargetsInGroup1); + + assertThat(groups.get(1).getStatus()).isEqualTo(RolloutGroupStatus.READY); + assertThat(groups.get(1).getTotalTargets()).isEqualTo(countTargetsInGroup2); + + assertThat(groups.get(2).getStatus()).isEqualTo(RolloutGroupStatus.READY); + assertThat(groups.get(2).getTotalTargets()).isEqualTo(countTargetsInGroup3); + + } + + @Test + @Description("Verify Exception when a Rollout with Group definition is created that does not address all targets") + public void createRolloutWithGroupsNotMatchingTargets() throws Exception { + final String rolloutName = "rolloutTest4"; + final int amountTargetsForRollout = 500; + final int percentTargetsInGroup1 = 20; + final int percentTargetsInGroup2 = 50; + + final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().build(); + Rollout myRollout = generateTargetsAndRollout(rolloutName, amountTargetsForRollout); + + List rolloutGroups = new ArrayList<>(2); + rolloutGroups.add(generateRolloutGroup(0, percentTargetsInGroup1, null)); + rolloutGroups.add(generateRolloutGroup(1, percentTargetsInGroup2, null)); + + try { + rolloutManagement.createRollout(myRollout, rolloutGroups, conditions); + fail("Was able to create a Rollout with groups that are not addressing all targets"); + } catch(RolloutVerificationException e) { + // OK + } + + } + + @Test + @Description("Verify Exception when a Rollout with Group definition is created that contains an illegal percentage") + public void createRolloutWithIllegalPercentage() throws Exception { + final String rolloutName = "rolloutTest6"; + final int amountTargetsForRollout = 10; + final int percentTargetsInGroup1 = 101; + final int percentTargetsInGroup2 = 50; + + final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().build(); + Rollout myRollout = generateTargetsAndRollout(rolloutName, amountTargetsForRollout); + + List rolloutGroups = new ArrayList<>(2); + rolloutGroups.add(generateRolloutGroup(0, percentTargetsInGroup1, null)); + rolloutGroups.add(generateRolloutGroup(1, percentTargetsInGroup2, null)); + + try { + rolloutManagement.createRollout(myRollout, rolloutGroups, conditions); + fail("Was able to create a Rollout with groups that have illegal percentages"); + } catch(RolloutVerificationException e) { + // OK + } + + } + + @Test + @Description("Verify Exception when a Rollout is created with too much groups") + public void createRolloutWithIllegalAmountOfGroups() throws Exception { + final String rolloutName = "rolloutTest5"; + final int amountTargetsForRollout = 10; + final int illegalGroupAmount = 501; + + final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().build(); + Rollout myRollout = generateTargetsAndRollout(rolloutName, amountTargetsForRollout); + + try { + rolloutManagement.createRollout(myRollout, illegalGroupAmount, conditions); + fail("Was able to create a Rollout with too many groups"); + } catch(RolloutVerificationException e) { + // OK + } + + } + + @Test + @Description("Verify the start of a Rollout does not work during creation phase.") + public void createAndStartRolloutDuringCreationFails() throws Exception { + final int amountTargetsForRollout = 3; + final int amountGroups = 5; + final String successCondition = "50"; + final String errorCondition = "80"; + final String rolloutName = "rolloutTestGC"; + final String targetPrefixName = rolloutName; + final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName); + targetManagement.createTargets( + testdataFactory.generateTargets(amountTargetsForRollout, targetPrefixName + "-", targetPrefixName)); + + final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder() + .successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition) + .errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition) + .errorAction(RolloutGroupErrorAction.PAUSE, null).build(); + final Rollout rolloutToCreate = new JpaRollout(); + rolloutToCreate.setName(rolloutName); + rolloutToCreate.setDescription("some description"); + rolloutToCreate.setTargetFilterQuery("id==" + targetPrefixName + "-*"); + rolloutToCreate.setDistributionSet(distributionSet); + + Rollout myRollout = rolloutManagement.createRollout(rolloutToCreate, amountGroups, conditions); + myRollout = rolloutManagement.findRolloutById(myRollout.getId()); + + assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.CREATING); + + try { + rolloutManagement.startRollout(myRollout); + fail("Was able to start a Rollout in CREATING status"); + } catch(RolloutIllegalStateException e) { + // OK + } + + } + + private RolloutGroup generateRolloutGroup(final int index, Integer percentage, String targetFilter) { + RolloutGroup group = entityFactory.generateRolloutGroup(); + group.setName("Group" + index); + group.setDescription("Group" + index + "desc"); + if (percentage != null) { + group.setTargetPercentage(percentage); + } + if (targetFilter != null) { + group.setTargetFilterQuery(targetFilter); + } + return group; + } + + private Rollout generateTargetsAndRollout(final String rolloutName, final int amountTargetsForRollout) { + final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName); + + targetManagement.createTargets( + testdataFactory.generateTargets(amountTargetsForRollout, rolloutName + "-", rolloutName)); + + Rollout myRollout = entityFactory.generateRollout(); + myRollout.setName(rolloutName); + myRollout.setDescription("This is a test description for the rollout"); + myRollout.setTargetFilterQuery("controllerId==" + rolloutName + "-*"); + myRollout.setDistributionSet(distributionSet); + + return myRollout; + } + private void validateRolloutGroupActionStatus(final RolloutGroup rolloutGroup, final Map expectedTargetCountStatus) { final RolloutGroup rolloutGroupWithDetail = rolloutGroupManagement @@ -1052,7 +1376,12 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { rolloutToCreate.setDescription(rolloutDescription); rolloutToCreate.setTargetFilterQuery(filterQuery); rolloutToCreate.setDistributionSet(distributionSet); - return rolloutManagement.createRollout(rolloutToCreate, groupSize, conditions); + final Rollout rollout = rolloutManagement.createRollout(rolloutToCreate, groupSize, conditions); + + // Run here, because Scheduler is disabled during tests + rolloutManagement.fillRolloutGroupsWithTargets(rolloutManagement.findRolloutById(rollout.getId())); + + return rolloutManagement.findRolloutById(rollout.getId()); } private int changeStatusForAllRunningActions(final Rollout rollout, final Status status) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLActionFieldsTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLActionFieldsTest.java index 884b02459..7afc46d5a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLActionFieldsTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLActionFieldsTest.java @@ -18,6 +18,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.DistributionSet; import org.junit.Before; import org.junit.Test; import org.springframework.data.domain.PageRequest; @@ -36,17 +37,20 @@ public class RSQLActionFieldsTest extends AbstractJpaIntegrationTest { @Before public void setupBeforeTest() { + final DistributionSet dsA = testdataFactory.createDistributionSet("daA"); target = new JpaTarget("targetId123"); target.setDescription("targetId123"); targetManagement.createTarget(target); action = new JpaAction(); action.setActionType(ActionType.SOFT); + action.setDistributionSet(dsA); target.addAction(action); action.setTarget(target); actionRepository.save(action); for (int i = 0; i < 10; i++) { final JpaAction newAction = new JpaAction(); newAction.setActionType(ActionType.SOFT); + newAction.setDistributionSet(dsA); newAction.setActive(i % 2 == 0); newAction.setTarget(target); actionRepository.save(newAction); diff --git a/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java b/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java index 0ee2c846d..b1c12eb53 100644 --- a/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java +++ b/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java @@ -16,6 +16,7 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; @@ -398,7 +399,13 @@ public abstract class JsonBuilder { } public static String rollout(final String name, final String description, final int groupSize, - final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions) { + final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions) { + return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, null); + } + + public static String rollout(final String name, final String description, final Integer groupSize, + final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions, + final List groups) { final JSONObject json = new JSONObject(); json.put("name", name); json.put("description", description); @@ -410,22 +417,64 @@ public abstract class JsonBuilder { final JSONObject successCondition = new JSONObject(); json.put("successCondition", successCondition); successCondition.put("condition", conditions.getSuccessCondition().toString()); - successCondition.put("expression", conditions.getSuccessConditionExp().toString()); + successCondition.put("expression", conditions.getSuccessConditionExp()); final JSONObject successAction = new JSONObject(); json.put("successAction", successAction); successAction.put("action", conditions.getSuccessAction().toString()); - successAction.put("expression", conditions.getSuccessActionExp().toString()); + successAction.put("expression", conditions.getSuccessActionExp()); final JSONObject errorCondition = new JSONObject(); json.put("errorCondition", errorCondition); errorCondition.put("condition", conditions.getErrorCondition().toString()); - errorCondition.put("expression", conditions.getErrorConditionExp().toString()); + errorCondition.put("expression", conditions.getErrorConditionExp()); final JSONObject errorAction = new JSONObject(); json.put("errorAction", errorAction); errorAction.put("action", conditions.getErrorAction().toString()); - errorAction.put("expression", conditions.getErrorActionExp().toString()); + errorAction.put("expression", conditions.getErrorActionExp()); + } + + if(groups != null) { + final JSONArray jsonGroups = new JSONArray(); + + for (RolloutGroup group : groups) { + final JSONObject jsonGroup = new JSONObject(); + jsonGroup.put("name", group.getName()); + jsonGroup.put("description", group.getDescription()); + jsonGroup.put("targetFilterQuery", group.getTargetFilterQuery()); + jsonGroup.put("targetPercentage", group.getTargetPercentage()); + + if(group.getSuccessCondition() != null) { + final JSONObject successCondition = new JSONObject(); + jsonGroup.put("successCondition", successCondition); + successCondition.put("condition", group.getSuccessCondition().toString()); + successCondition.put("expression", group.getSuccessConditionExp()); + } + if(group.getSuccessAction() != null) { + final JSONObject successAction = new JSONObject(); + jsonGroup.put("successAction", successAction); + successAction.put("action", group.getSuccessAction().toString()); + successAction.put("expression", group.getSuccessActionExp()); + } + if(group.getErrorCondition() != null) { + final JSONObject errorCondition = new JSONObject(); + jsonGroup.put("errorCondition", errorCondition); + errorCondition.put("condition", group.getErrorCondition().toString()); + errorCondition.put("expression", group.getErrorConditionExp()); + } + if(group.getErrorAction() != null) { + final JSONObject errorAction = new JSONObject(); + jsonGroup.put("errorAction", errorAction); + errorAction.put("action", group.getErrorAction().toString()); + errorAction.put("expression", group.getErrorActionExp()); + } + + jsonGroups.put(jsonGroup); + } + + json.put("groups", jsonGroups); + } return json.toString(); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java index dae322b2e..461a30031 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java @@ -491,7 +491,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { rolloutToCreate.setActionType(getActionType()); rolloutToCreate.setForcedTime(getForcedTimeStamp()); - rolloutToCreate = rolloutManagement.createRolloutAsync(rolloutToCreate, amountGroup, conditions); + rolloutToCreate = rolloutManagement.createRollout(rolloutToCreate, amountGroup, conditions); return rolloutToCreate; } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java index c4db28f6f..721020eac 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java @@ -414,7 +414,7 @@ public class RolloutListGrid extends AbstractGrid { final String rolloutName = (String) row.getItemProperty(SPUILabelDefinitions.VAR_NAME).getValue(); if (RolloutStatus.READY.equals(rolloutStatus)) { - rolloutManagement.startRolloutAsync(rolloutManagement.findRolloutByName(rolloutName)); + rolloutManagement.startRollout(rolloutManagement.findRolloutByName(rolloutName)); uiNotification.displaySuccess(i18n.get("message.rollout.started", rolloutName)); return; }