Support user consent flow (#1293)

* Introduce user consent flow
* Add permissions to confirmation management
* rename from consent to confirmation
* Reformat code. Remove unused imports. Change and add permission checks when configuring auto-confirmation.
* Do not include null values for DDI confirmation base endpoint
* fix confirmation required checkbox id
* Remove unused import. Fix consume/produce type of new API's.
* Change term processing to proceeding when activating user consent flow
* Align formatting and extend integration test cases for DMF and DDI.
* Extend DMF test cases to consider auto-confirmation
* Refactor action management to fix problem of handling action status updates on closed actions.
* remove unsupported validation
* use new confirmation api for DMF. Extend test cases.,
* Remove unnecessary fields.
* Extend API documentation for DDI and MGMT API.
* adapt ddi api docs adoc file
* Fixed the duplicate migration version for db files
* fix method to support confirmation
* Fixed PR comments
* Addressed PR comments
* Fixed after merge compilation issue
* Fixed after merge compilation issue
* Fix failing tests in MgmtRolloutResourceTest
* Fixed the permissions issue reflected by integration tests
* Added back the missing line of code lost during merge
* Fix the failing test on Jenkins

Signed-off-by: Stanislav Trailov <stanislav.trailov@bosch.io>
Signed-off-by: Dimitar Shterev <dimitar.shterev@bosch.io>
Signed-off-by: Michael Herdt <Michael.Herdt@bosch.io>
Signed-off-by: Shruthi Manavalli Ramanna <shruthimanavalli.ramanna@bosch-si.com>
Co-authored-by: Shruthi Manavalli Ramanna <shruthimanavalli.ramanna@bosch-si.com>
This commit is contained in:
Michael Herdt
2023-01-25 12:11:05 +01:00
committed by GitHub
parent b919ceda5c
commit 21f1569881
208 changed files with 7830 additions and 831 deletions

View File

@@ -0,0 +1,115 @@
/**
* Copyright (c) 2022 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.springframework.security.access.prepost.PreAuthorize;
import javax.validation.constraints.NotEmpty;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
/**
* Service layer for all confirmation related operations.
*/
public interface ConfirmationManagement {
/**
* Find active actions in the {@link Action.Status#WAIT_FOR_CONFIRMATION} state
* for a specific target with a specified controllerId.
*
* @param controllerId
* of the target to check
* @return a list of {@link Action}
*/
@PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET)
List<Action> findActiveActionsWaitingConfirmation(@NotEmpty String controllerId);
/**
* Activate auto confirmation for a given controller ID. In case auto
* confirmation is active already, this method will fail with an exception.
*
* @param controllerId
* to activate the feature for
* @param initiator
* who initiated this operation. If 'null' we will take the current
* user from {@link TenantAware#getCurrentUsername()}
* @param remark
* optional field to set a remark
* @return the persisted {@link AutoConfirmationStatus}
*/
@PreAuthorize(SpPermission.SpringEvalExpressions.IS_CONTROLLER_OR_HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
AutoConfirmationStatus activateAutoConfirmation(@NotEmpty String controllerId, final String initiator,
final String remark);
/**
* Get the current state of auto-confirmation for a given controllerId
*
* @param controllerId
* to check the state for
* @return instance of {@link AutoConfirmationStatus} wrapped in an
* {@link Optional}. Present if active and empty if disabled.
*/
@PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET)
Optional<AutoConfirmationStatus> getStatus(@NotEmpty String controllerId);
/**
* Auto confirm active actions for a specific controller ID having the
* {@link Action.Status#WAIT_FOR_CONFIRMATION} status.
*
* @param controllerId
* to confirm actions for
* @return a list of confirmed actions
*/
@PreAuthorize(SpPermission.SpringEvalExpressions.IS_CONTROLLER_OR_HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
List<Action> autoConfirmActiveActions(@NotEmpty String controllerId);
/**
* Confirm a given action to put it from
* {@link Action.Status#WAIT_FOR_CONFIRMATION} to {@link Action.Status#RUNNING}
* state.
*
* @param actionId
* mandatory to know which action to confirm
* @param code
* optional value to specify a code for the created action status
* @param messages
* optional value to specify message for the created action status
*/
@PreAuthorize(SpPermission.SpringEvalExpressions.IS_CONTROLLER_OR_HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
Action confirmAction(long actionId, Integer code, Collection<String> messages);
/**
* Deny a given action and leave it in
* {@link Action.Status#WAIT_FOR_CONFIRMATION} state.
*
* @param actionId
* mandatory to know which action to deny
* @param code
* optional value to specify a code for the created action status
* @param messages
* optional value to specify message for the created action status
*/
@PreAuthorize(SpPermission.SpringEvalExpressions.IS_CONTROLLER_OR_HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
Action denyAction(long actionId, Integer code, Collection<String> messages);
/**
* Deactivate auto confirmation for a specific controller id
*
* @param controllerId
* to disable auto confirmation for
*/
@PreAuthorize(SpPermission.SpringEvalExpressions.IS_CONTROLLER_OR_HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
void deactivateAutoConfirmation(@NotEmpty String controllerId);
}

View File

@@ -29,6 +29,7 @@ import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
import org.eclipse.hawkbit.repository.model.Target;
@@ -508,4 +509,28 @@ public interface ControllerManagement {
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
Optional<Action> getInstalledActionByTarget(@NotEmpty String controllerId);
/**
* Activate auto confirmation for a given controllerId
*
* @param controllerId
* to activate auto-confirmation on
* @param initiator
* can be set optionally (fallback is the current acting security
* user)
* @param remark
* (optional) remark
* @return the persisted {@link AutoConfirmationStatus}
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
AutoConfirmationStatus activateAutoConfirmation(@NotEmpty String controllerId, String initiator, String remark);
/**
* Deactivate auto confirmation for a given controllerId
*
* @param controllerId
* to deactivate auto-confirmation on
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
void deactivateAutoConfirmation(@NotEmpty String controllerId);
}

View File

@@ -449,8 +449,8 @@ public interface DeploymentManagement {
Page<Action> findInActiveActionsByTarget(@NotNull Pageable pageable, @NotEmpty String controllerId);
/**
* Retrieves active {@link Action}s with highest weight that are assigned to
* a {@link Target}.
* Retrieves active {@link Action}s with highest weight that are assigned to a
* {@link Target}.
*
* @param controllerId
* identifies the target to retrieve the action from
@@ -522,6 +522,8 @@ public interface DeploymentManagement {
*
* @param rolloutId
* the rollout the actions belong to
* @param distributionSetId
* to assign
* @param rolloutGroupParentId
* the parent rollout group the actions should reference. null
* references the first group

View File

@@ -108,27 +108,28 @@ 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 amount of groups will
* be defined as equally sized.
* {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets 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,
* creating and persisting all the necessary groups. The Rollout and the
* groups are persisted in {@link RolloutStatus#CREATING} and
* 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 #start(Rollout)}.
* targets have been assigned to the groups, the rollout status is changed to
* {@link RolloutStatus#READY} so it can be started with .
*
* @param create
* the rollout entity to create
* @param amountGroup
* the amount of groups to split the rollout into
* @param confirmationRequired
* if a confirmation is required by the device group(s) of the rollout
* @param conditions
* the rolloutgroup conditions and actions which should be
* applied for each {@link RolloutGroup}
* the rolloutgroup conditions and actions which should be applied
* for each {@link RolloutGroup}
* @return the persisted rollout.
*
* @throws EntityNotFoundException
@@ -140,22 +141,23 @@ public interface RolloutManagement {
* exceeded.
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_CREATE)
Rollout create(@NotNull @Valid RolloutCreate create, int amountGroup, @NotNull RolloutGroupConditions conditions);
Rollout create(@NotNull @Valid RolloutCreate create, int amountGroup, boolean confirmationRequired,
@NotNull RolloutGroupConditions conditions);
/**
* Persists a new rollout entity. The filter within the
* {@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.
* {@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,
* creating and persisting all the necessary groups. The Rollout and the
* groups are persisted in {@link RolloutStatus#CREATING} and
* 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
* targets have been assigned to the groups, the rollout status is changed to
* {@link RolloutStatus#READY} so it can be started with
* {@link #start(Rollout)}.
*
* @param rollout
@@ -163,9 +165,9 @@ public interface RolloutManagement {
* @param groups
* optional definition of groups
* @param conditions
* the rollout group conditions and actions which should be
* applied for each {@link RolloutGroup} if not defined by the
* RolloutGroup itself
* 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 EntityNotFoundException

View File

@@ -29,6 +29,8 @@ public class AutoAssignDistributionSetUpdate {
@Max(Action.WEIGHT_MAX)
private Integer weight;
private Boolean confirmationRequired;
/**
* Constructor
*
@@ -75,6 +77,19 @@ public class AutoAssignDistributionSetUpdate {
return this;
}
/**
* Specify initial confirmation state of resulting {@link Action}
*
* @param confirmationRequired
* if confirmation is required for this auto assignment (considered
* with confirmation flow active)
* @return updated builder instance
*/
public AutoAssignDistributionSetUpdate confirmationRequired(final boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
return this;
}
public Long getDsId() {
return dsId;
}
@@ -87,6 +102,10 @@ public class AutoAssignDistributionSetUpdate {
return weight;
}
public Boolean isConfirmationRequired() {
return confirmationRequired;
}
public long getTargetFilterId() {
return targetFilterId;
}

View File

@@ -63,6 +63,14 @@ public interface RolloutGroupCreate {
*/
RolloutGroupCreate conditions(RolloutGroupConditions conditions);
/**
* @param confirmationRequired
* if confirmation is required for this rollout group (considered
* with confirmation flow active)
* @return updated builder instance
*/
RolloutGroupCreate confirmationRequired(boolean confirmationRequired);
/**
* @return peek on current state of {@link RolloutGroup} in the builder
*/

View File

@@ -83,6 +83,17 @@ public interface TargetFilterQueryCreate {
*/
TargetFilterQueryCreate autoAssignWeight(Integer weight);
/**
* Specify initial confirmation state of resulting {@link Action} in auto
* assignment
*
* @param confirmationRequired
* if confirmation is required for configured auto assignment (considered
* with confirmation flow active)
* @return updated builder instance
*/
TargetFilterQueryCreate confirmationRequired(boolean confirmationRequired);
/**
* @return peek on current state of {@link TargetFilterQuery} in the builder
*/

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2022 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.exception;
import org.eclipse.hawkbit.exception.AbstractServerRtException;
import org.eclipse.hawkbit.exception.SpServerError;
/**
* The {@link AutoConfirmationAlreadyActiveException} is thrown when auto
* confirmation is already active for a device but the
* {@link org.eclipse.hawkbit.repository.ConfirmationManagement#activateAutoConfirmation}
* is getting called.
*/
public class AutoConfirmationAlreadyActiveException extends AbstractServerRtException {
private static final long serialVersionUID = 1L;
private static final SpServerError THIS_ERROR = SpServerError.SP_REPO_AUTO_CONFIRMATION_ALREADY_ACTIVE;
/**
* Default constructor.
*/
public AutoConfirmationAlreadyActiveException() {
super(THIS_ERROR);
}
/**
* Parameterized constructor.
*
* @param cause
* of the exception
*/
public AutoConfirmationAlreadyActiveException(final Throwable cause) {
super(THIS_ERROR, cause);
}
/**
* Parameterized constructor for auto confirmation is already active for given
* controller ID
*
* @param controllerId
* of affected device
*/
public AutoConfirmationAlreadyActiveException(final String controllerId) {
super("Auto confirmation is already active for device " + controllerId, THIS_ERROR);
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2022 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.exception;
import org.eclipse.hawkbit.exception.AbstractServerRtException;
import org.eclipse.hawkbit.exception.SpServerError;
/**
* This exception is indicating that the confirmation feedback cannot be
* processed for a specific actions for different reasons which are listed as
* enum {@link Reason}.
*/
public class InvalidConfirmationFeedbackException extends AbstractServerRtException {
private static final long serialVersionUID = 1L;
private static final SpServerError THIS_ERROR = SpServerError.SP_CONFIRMATION_FEEDBACK_INVALID;
private final Reason reason;
protected InvalidConfirmationFeedbackException(final Reason reason) {
super(THIS_ERROR);
this.reason = reason;
}
public InvalidConfirmationFeedbackException(final Reason reason, final String message) {
super(message, THIS_ERROR);
this.reason = reason;
}
public Reason getReason() {
return reason;
}
public enum Reason {
ACTION_CLOSED, NOT_AWAITING_CONFIRMATION
}
}

View File

@@ -259,7 +259,12 @@ public interface Action extends TenantAwareBaseEntity {
* Action has been downloaded by the target and waiting for update to
* start.
*/
DOWNLOADED
DOWNLOADED,
/**
* Action is waiting to be confirmed by the user
*/
WAIT_FOR_CONFIRMATION
}
/**
@@ -330,4 +335,11 @@ public interface Action extends TenantAwareBaseEntity {
* @return true if maintenance window is available, else false.
*/
boolean isMaintenanceWindowAvailable();
/**
* Checks if the action is waiting for confirmation.
* @return true if the action is waiting for confirmation, else false
*/
boolean isWaitingConfirmation();
}

View File

@@ -8,6 +8,8 @@
*/
package org.eclipse.hawkbit.repository.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.io.Serializable;
/**
@@ -22,6 +24,8 @@ public class ActionProperties implements Serializable {
private String tenant;
private boolean maintenanceWindowAvailable;
private Action.Status status;
public ActionProperties() {
}
@@ -35,6 +39,7 @@ public class ActionProperties implements Serializable {
this.actionType = action.getActionType();
this.tenant = action.getTenant();
this.maintenanceWindowAvailable = action.isMaintenanceWindowAvailable();
this.status = action.getStatus();
}
public void setId(final Long id) {
@@ -68,4 +73,13 @@ public class ActionProperties implements Serializable {
public void setActionType(final Action.ActionType actionType) {
this.actionType = actionType;
}
public Action.Status getStatus() {
return status;
}
@JsonIgnore
public boolean isWaitingConfirmation() {
return status == Action.Status.WAIT_FOR_CONFIRMATION;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2022 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.model;
/**
* {@link AutoConfirmationStatus} of a {@link Target}.
*
*/
public interface AutoConfirmationStatus extends BaseEntity {
/**
* For which target this status is corresponding to.
*
* @return the {@link Target}
*/
Target getTarget();
/**
* The user who initiated the auto confirmation. Will be set on auto
* confirmation activation and could be null. In this case the created_by can be
* considered as initiator.
*
* @return the user
*/
String getInitiator();
/**
* Unix timestamp of the activation.
*
* @return activation time as unix timestamp
*/
long getActivatedAt();
/**
* Optional value, which can be set during activation.
*
* @return the remark
*/
String getRemark();
/**
* Construct the action message based on the current status.
*
* @return the constructed message which can be used for the action status as a
* message
*/
String constructActionMessage();
}

View File

@@ -25,8 +25,8 @@ public class DeploymentRequest {
private final TargetWithActionType targetWithActionType;
/**
* Constructor that also accepts maintenance schedule parameters and checks
* for validity of the specified maintenance schedule.
* Constructor that also accepts maintenance schedule parameters and checks for
* validity of the specified maintenance schedule.
*
* @param controllerId
* for which the action is created.
@@ -48,18 +48,30 @@ public class DeploymentRequest {
* window, for example 00:30:00 for 30 minutes
* @param maintenanceWindowTimeZone
* is the time zone specified as +/-hh:mm offset from UTC, for
* example +02:00 for CET summer time and +00:00 for UTC. The
* start time of a maintenance window calculated based on the
* cron expression is relative to this time zone.
* example +02:00 for CET summer time and +00:00 for UTC. The start
* time of a maintenance window calculated based on the cron
* expression is relative to this time zone.
*
* @param confirmationRequired
* is a flag whether the confirmation should be required for the
* resulting {@link Action} or not. In case the confirmation is not
* required, the action will be automatically confirmed and put in
* the
* {@link org.eclipse.hawkbit.repository.model.Action.Status#RUNNING}
* state. Otherwise the confirmation flow will be triggered
* and the {@link Action} will stay in the
* {@link org.eclipse.hawkbit.repository.model.Action.Status#WAIT_FOR_CONFIRMATION}
* state until the confirmation is given. (Only considered
* with CONFIRMATION_FLOW active via tenant configuration)
* @throws InvalidMaintenanceScheduleException
* if the parameters do not define a valid maintenance schedule.
*/
public DeploymentRequest(final String controllerId, final Long distributionSetId, final ActionType actionType,
final long forceTime, final Integer weight, final String maintenanceSchedule,
final String maintenanceWindowDuration, final String maintenanceWindowTimeZone) {
final String maintenanceWindowDuration, final String maintenanceWindowTimeZone,
final boolean confirmationRequired) {
this.targetWithActionType = new TargetWithActionType(controllerId, actionType, forceTime, weight,
maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone);
maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone, confirmationRequired);
this.distributionSetId = distributionSetId;
}
@@ -78,11 +90,11 @@ public class DeploymentRequest {
@Override
public String toString() {
return String.format(
"DeploymentRequest [controllerId=%s, distributionSetId=%d, actionType=%s, forceTime=%d, weight=%d, maintenanceSchedule=%s, maintenanceWindowDuration=%s, maintenanceWindowTimeZone=%s]",
"DeploymentRequest [controllerId=%s, distributionSetId=%d, actionType=%s, forceTime=%d, weight=%d, maintenanceSchedule=%s, maintenanceWindowDuration=%s, maintenanceWindowTimeZone=%s, confirmationRequired=%s]",
targetWithActionType.getControllerId(), getDistributionSetId(), targetWithActionType.getActionType(),
targetWithActionType.getForceTime(), targetWithActionType.getWeight(),
targetWithActionType.getMaintenanceSchedule(), targetWithActionType.getMaintenanceWindowDuration(),
targetWithActionType.getMaintenanceWindowTimeZone());
targetWithActionType.getMaintenanceWindowTimeZone(), targetWithActionType.isConfirmationRequired());
}
@Override

View File

@@ -24,6 +24,7 @@ public class DeploymentRequestBuilder {
private String maintenanceSchedule;
private String maintenanceWindowDuration;
private String maintenanceWindowTimeZone;
private boolean confirmationRequired;
/**
* Create a builder for a target distribution set assignment with the
@@ -100,6 +101,18 @@ public class DeploymentRequestBuilder {
return this;
}
/**
* Set if a confirmation is required.
*
* @param confirmationRequired
* if a confirmation is required for the {@link Action}
* @return builder
*/
public DeploymentRequestBuilder setConfirmationRequired(final boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
return this;
}
/**
* build the request
*
@@ -107,7 +120,7 @@ public class DeploymentRequestBuilder {
*/
public DeploymentRequest build() {
return new DeploymentRequest(controllerId, distributionSetId, actionType, forceTime, weight,
maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone);
maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone, confirmationRequired);
}
}

View File

@@ -109,6 +109,12 @@ public interface RolloutGroup extends NamedEntity {
*/
float getTargetPercentage();
/**
* @return if a confirmation is required for the resulting actions (considered
* with confirmation flow active only)
*/
boolean isConfirmationRequired();
/**
* Rollout group state machine.
*

View File

@@ -98,6 +98,14 @@ public interface Target extends NamedEntity {
*/
PollStatus getPollStatus();
/**
* The auto confirmation status is present, when it's active for the target.
* Will only be considered in case the confirmation flow is active.
*
* @return the {@link AutoConfirmationStatus} if activated
*/
AutoConfirmationStatus getAutoConfirmationStatus();
/**
* @return <code>true</code> if the {@link Target} has not jet provided
* {@link #getControllerAttributes()}.

View File

@@ -81,4 +81,10 @@ public interface TargetFilterQuery extends TenantAwareBaseEntity {
* @return the user that triggered the auto assignment
*/
String getAutoAssignInitiatedBy();
/**
* @return if confirmation is required for configured auto assignment
* (considered with confirmation flow active)
*/
boolean isConfirmationRequired();
}

View File

@@ -30,6 +30,7 @@ public class TargetWithActionType {
private String maintenanceSchedule;
private String maintenanceWindowDuration;
private String maintenanceWindowTimeZone;
private final boolean confirmationRequired;
/**
* Constructor that uses {@link ActionType#FORCED}
@@ -38,7 +39,7 @@ public class TargetWithActionType {
* ID if the controller
*/
public TargetWithActionType(final String controllerId) {
this(controllerId, ActionType.FORCED, 0, null);
this(controllerId, ActionType.FORCED, 0, null, false);
}
/**
@@ -49,17 +50,22 @@ public class TargetWithActionType {
* @param actionType
* specified for the action.
* @param forceTime
* after that point in time the action is exposed as forced in
* case the type is {@link ActionType#TIMEFORCED}
* after that point in time the action is exposed as forced in case
* the type is {@link ActionType#TIMEFORCED}
* @param weight
* the priority of an {@link Action}
* @param confirmationRequired
* sets the confirmation required flag when starting the
* {@link Action}
*/
public TargetWithActionType(final String controllerId, final ActionType actionType, final long forceTime,
final Integer weight) {
public TargetWithActionType(
final String controllerId, final ActionType actionType, final long forceTime,
final Integer weight, final boolean confirmationRequired) {
this.controllerId = controllerId;
this.actionType = actionType != null ? actionType : ActionType.FORCED;
this.forceTime = forceTime;
this.weight = weight;
this.confirmationRequired = confirmationRequired;
}
/**
@@ -93,8 +99,8 @@ public class TargetWithActionType {
*/
public TargetWithActionType(final String controllerId, final ActionType actionType, final long forceTime,
final Integer weight, final String maintenanceSchedule, final String maintenanceWindowDuration,
final String maintenanceWindowTimeZone) {
this(controllerId, actionType, forceTime, weight);
final String maintenanceWindowTimeZone, final boolean confirmationRequired) {
this(controllerId, actionType, forceTime, weight, confirmationRequired);
this.maintenanceSchedule = maintenanceSchedule;
this.maintenanceWindowDuration = maintenanceWindowDuration;
@@ -152,18 +158,27 @@ public class TargetWithActionType {
return maintenanceWindowTimeZone;
}
/**
* Return if a confirmation is required for this assignment (depends on confirmation flow active)
*
* @return the flag
*/
public boolean isConfirmationRequired() {
return confirmationRequired;
}
@Override
public String toString() {
return "TargetWithActionType [controllerId=" + controllerId + ", actionType=" + getActionType() + ", forceTime="
+ getForceTime() + ", weight=" + getWeight() + ", maintenanceSchedule=" + getMaintenanceSchedule()
+ ", maintenanceWindowDuration=" + getMaintenanceWindowDuration() + ", maintenanceWindowTimeZone="
+ getMaintenanceWindowTimeZone() + "]";
+ getMaintenanceWindowTimeZone() + ", confirmationRequired=" + isConfirmationRequired() + "]";
}
@Override
public int hashCode() {
return Objects.hash(actionType, controllerId, forceTime, weight, maintenanceSchedule, maintenanceWindowDuration,
maintenanceWindowTimeZone);
return Objects.hash(actionType, controllerId, forceTime, weight, confirmationRequired, maintenanceSchedule,
maintenanceWindowDuration, maintenanceWindowTimeZone);
}
@SuppressWarnings("squid:S1067")
@@ -181,8 +196,10 @@ public class TargetWithActionType {
final TargetWithActionType other = (TargetWithActionType) obj;
return Objects.equals(actionType, other.actionType) && Objects.equals(controllerId, other.controllerId)
&& Objects.equals(forceTime, other.forceTime) && Objects.equals(weight, other.weight)
&& Objects.equals(confirmationRequired, other.confirmationRequired)
&& Objects.equals(maintenanceSchedule, other.maintenanceSchedule)
&& Objects.equals(maintenanceWindowDuration, other.maintenanceWindowDuration)
&& Objects.equals(maintenanceWindowTimeZone, other.maintenanceWindowTimeZone);
}
}

View File

@@ -161,6 +161,7 @@ public class TotalTargetCountStatus {
case RUNNING:
case WARNING:
case DOWNLOAD:
case WAIT_FOR_CONFIRMATION:
case CANCELING:
return Status.RUNNING;
case DOWNLOADED:

View File

@@ -152,6 +152,11 @@ public class TenantConfigurationProperties {
*/
public static final String BATCH_ASSIGNMENTS_ENABLED = "batch.assignments.enabled";
/**
* Switch to enable/disable the user-confirmation flow
*/
public static final String USER_CONFIRMATION_ENABLED = "user.confirmation.flow.enabled";
private String keyName;
private String defaultValue = "";
private Class<?> dataType = String.class;

View File

@@ -6,9 +6,10 @@
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.jpa.utils;
package org.eclipse.hawkbit.utils;
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED;
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.USER_CONFIRMATION_ENABLED;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
@@ -50,4 +51,14 @@ public final class TenantConfigHelper {
return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement
.getConfigurationValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class).getValue());
}
/**
* Is confirmation flow enabled for the current tenant
*
* @return is enabled
*/
public boolean isConfirmationFlowEnabled() {
return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement
.getConfigurationValue(USER_CONFIRMATION_ENABLED, Boolean.class).getValue());
}
}

View File

@@ -23,6 +23,7 @@ public abstract class AbstractRolloutGroupCreate<T> extends AbstractNamedEntityB
protected String targetFilterQuery;
protected Float targetPercentage;
protected RolloutGroupConditions conditions;
protected boolean confirmationRequired;
public T targetFilterQuery(final String targetFilterQuery) {
this.targetFilterQuery = StringUtils.trimWhitespace(targetFilterQuery);
@@ -39,4 +40,9 @@ public abstract class AbstractRolloutGroupCreate<T> extends AbstractNamedEntityB
return (T) this;
}
public T confirmationRequired(final boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
return (T) this;
}
}

View File

@@ -39,6 +39,8 @@ public abstract class AbstractTargetFilterQueryUpdateCreate<T> extends AbstractB
@Min(Action.WEIGHT_MIN)
@Max(Action.WEIGHT_MAX)
protected Integer weight;
protected Boolean confirmationRequired;
/**
* Set DS ID of the {@link Action} created during auto assignment
@@ -120,4 +122,19 @@ public abstract class AbstractTargetFilterQueryUpdateCreate<T> extends AbstractB
public Optional<String> getQuery() {
return Optional.ofNullable(query);
}
/**
* @param confirmationRequired
* if confirmation is required for configured auto assignment
* (considered with confirmation flow active)
* @return updated builder instance
*/
public T confirmationRequired(final boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
return (T) this;
}
public Optional<Boolean> getConfirmationRequired() {
return Optional.ofNullable(confirmationRequired);
}
}

View File

@@ -101,5 +101,9 @@ hawkbit.server.tenant.configuration.batch-assignments-enabled.defaultValue=false
hawkbit.server.tenant.configuration.batch-assignments-enabled.dataType=java.lang.Boolean
hawkbit.server.tenant.configuration.batch-assignments-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator
hawkbit.server.tenant.configuration.user-confirmation-enabled.keyName=user.confirmation.flow.enabled
hawkbit.server.tenant.configuration.user-confirmation-enabled.defaultValue=false
hawkbit.server.tenant.configuration.user-confirmation-enabled.dataType=java.lang.Boolean
hawkbit.server.tenant.configuration.user-confirmation-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator
# Default tenant configuration - END

View File

@@ -57,11 +57,13 @@ public abstract class AbstractDsAssignmentStrategy {
private final ActionStatusRepository actionStatusRepository;
private final QuotaManagement quotaManagement;
private final BooleanSupplier multiAssignmentsConfig;
private final BooleanSupplier confirmationFlowConfig;
AbstractDsAssignmentStrategy(final TargetRepository targetRepository,
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig) {
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig,
final BooleanSupplier confirmationFlowConfig) {
this.targetRepository = targetRepository;
this.afterCommit = afterCommit;
this.eventPublisherHolder = eventPublisherHolder;
@@ -69,6 +71,7 @@ public abstract class AbstractDsAssignmentStrategy {
this.actionStatusRepository = actionStatusRepository;
this.quotaManagement = quotaManagement;
this.multiAssignmentsConfig = multiAssignmentsConfig;
this.confirmationFlowConfig = confirmationFlowConfig;
}
/**
@@ -276,4 +279,8 @@ public abstract class AbstractDsAssignmentStrategy {
protected boolean isMultiAssignmentsEnabled() {
return multiAssignmentsConfig.getAsBoolean();
}
protected boolean isConfirmationFlowEnabled() {
return confirmationFlowConfig.getAsBoolean();
}
}

View File

@@ -233,8 +233,8 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
/**
* Retrieves all {@link Action}s of a specific target and given active flag
* ordered by action ID. Loads also the lazy
* {@link Action#getDistributionSet()} field.
* ordered by action ID. Loads also the lazy {@link Action#getDistributionSet()}
* field.
*
* @param pageable
* page parameters
@@ -251,9 +251,8 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
@Param("active") boolean active);
/**
* Switches the status of actions from one specific status into another,
* only if the actions are in a specific status. This should be a atomar
* operation.
* Switches the status of actions from one specific status into another, only if
* the actions are in a specific status. This should be a atomar operation.
*
* @param statusToSet
* the new status the actions should get
@@ -270,6 +269,21 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
void switchStatus(@Param("statusToSet") Action.Status statusToSet, @Param("targetsIds") List<Long> targetIds,
@Param("active") boolean active, @Param("currentStatus") Action.Status currentStatus);
/**
*
* Retrieves all active {@link Action}s by given controllerId filtered by a
* status
*
* @param controllerId
* the IDs of targets for the actions
* @param status
* the current status of the actions
* @return the found list of {@link Action}
*/
@Query("SELECT a FROM JpaAction a WHERE a.target.controllerId = :controllerId AND a.active = true AND a.status = :status")
List<JpaAction> findByTargetIdAndIsActiveAndActionStatus(@Param("controllerId") String controllerId,
@Param("status") Action.Status status);
/**
*
* Retrieves all IDs for {@link Action}s referring to the given target IDs,
@@ -490,8 +504,8 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
boolean existsByRolloutIdAndStatusNotIn(@Param("rolloutId") Long rolloutId, @Param("status") Status status);
/**
* Retrieving all actions referring to a given rollout with a specific
* action as parent reference and a specific status.
* 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.
*
@@ -506,7 +520,7 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
* @return the actions referring a specific rollout and a specific parent
* rolloutgroup in a specific status
*/
@EntityGraph(attributePaths = { "target" }, type = EntityGraphType.LOAD)
@EntityGraph(attributePaths = { "target", "target.autoConfirmationStatus", "rolloutGroup" }, type = EntityGraphType.LOAD)
Page<Action> findByRolloutIdAndRolloutGroupParentIdAndStatus(Pageable pageable, Long rollout,
Long rolloutGroupParent, Status actionStatus);
@@ -522,7 +536,8 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
* @return the actions referring a specific rollout and a specific parent
* rolloutgroup in a specific status
*/
@EntityGraph(attributePaths = { "target" }, type = EntityGraphType.LOAD)
@EntityGraph(attributePaths = { "target", "target.autoConfirmationStatus",
"rolloutGroup" }, type = EntityGraphType.LOAD)
Page<Action> findByRolloutIdAndRolloutGroupParentIsNullAndStatus(Pageable pageable, Long rollout,
Status actionStatus);

View File

@@ -14,24 +14,44 @@ import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.jpa.builder.JpaActionStatusCreate;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.PageRequest;
import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY;
import static org.eclipse.hawkbit.repository.model.Action.Status.FINISHED;
/**
* Implements utility methods for managing {@link Action}s
*/
public class JpaActionManagement {
private static final Logger LOG = LoggerFactory.getLogger(JpaActionManagement.class);
protected final ActionRepository actionRepository;
protected final ActionStatusRepository actionStatusRepository;
protected final QuotaManagement quotaManagement;
protected final RepositoryProperties repositoryProperties;
protected JpaActionManagement(final ActionRepository actionRepository,
final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement,
final RepositoryProperties repositoryProperties) {
this.actionRepository = actionRepository;
this.actionStatusRepository = actionStatusRepository;
this.quotaManagement = quotaManagement;
this.repositoryProperties = repositoryProperties;
}
protected List<Action> findActiveActionsWithHighestWeightConsideringDefault(final String controllerId,
final int maxActionCount) {
if (!actionRepository.activeActionExistsForControllerId(controllerId)) {
@@ -50,8 +70,90 @@ public class JpaActionManagement {
return actions.stream().sorted(actionImportance).limit(maxActionCount).collect(Collectors.toList());
}
protected List<JpaAction> findActiveActionsHavingStatus(final String controllerId, final Action.Status status) {
if (!actionRepository.activeActionExistsForControllerId(controllerId)) {
return Collections.emptyList();
}
return Collections
.unmodifiableList(actionRepository.findByTargetIdAndIsActiveAndActionStatus(controllerId, status));
}
protected Action addActionStatus(final JpaActionStatusCreate statusCreate) {
final Long actionId = statusCreate.getActionId();
final JpaActionStatus actionStatus = statusCreate.build();
final JpaAction action = getActionAndThrowExceptionIfNotFound(actionId);
if (isUpdatingActionStatusAllowed(action, actionStatus)) {
return handleAddUpdateActionStatus(actionStatus, action);
}
LOG.debug("Update of actionStatus {} for action {} not possible since action not active anymore.",
actionStatus.getStatus(), action.getId());
return action;
}
/**
* ActionStatus updates are allowed mainly if the action is active. If the
* action is not active we accept further status updates if permitted so by
* repository configuration. In this case, only the values: Status.ERROR and
* Status.FINISHED are allowed. In the case of a DOWNLOAD_ONLY action, we accept
* status updates only once.
*/
protected boolean isUpdatingActionStatusAllowed(final JpaAction action, final JpaActionStatus actionStatus) {
final boolean isIntermediateFeedback = (FINISHED != actionStatus.getStatus())
&& (Action.Status.ERROR != actionStatus.getStatus());
final boolean isAllowedByRepositoryConfiguration = !repositoryProperties.isRejectActionStatusForClosedAction()
&& isIntermediateFeedback;
final boolean isAllowedForDownloadOnlyActions = isDownloadOnly(action) && !isIntermediateFeedback;
return action.isActive() || isAllowedByRepositoryConfiguration || isAllowedForDownloadOnlyActions;
}
protected int getWeightConsideringDefault(final Action action) {
return action.getWeight().orElse(repositoryProperties.getActionWeightIfAbsent());
}
protected JpaAction getActionAndThrowExceptionIfNotFound(final Long actionId) {
return actionRepository.findById(actionId)
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
}
protected static boolean isDownloadOnly(final JpaAction action) {
return DOWNLOAD_ONLY == action.getActionType();
}
/**
* Sets {@link TargetUpdateStatus} based on given {@link ActionStatus}.
*/
protected Action handleAddUpdateActionStatus(final JpaActionStatus actionStatus, final JpaAction action) {
// information status entry - check for a potential DOS attack
assertActionStatusQuota(action);
assertActionStatusMessageQuota(actionStatus);
actionStatus.setAction(action);
onActionStatusUpdate(actionStatus.getStatus(), action);
actionStatusRepository.save(actionStatus);
action.setLastActionStatusCode(actionStatus.getCode().orElse(null));
return actionRepository.save(action);
}
protected void onActionStatusUpdate(final Action.Status updatedActionStatus, final JpaAction action){
// can be overwritten to intercept the persistence of the action status
}
protected void assertActionStatusQuota(final JpaAction action) {
QuotaHelper.assertAssignmentQuota(action.getId(), 1, quotaManagement.getMaxStatusEntriesPerAction(),
ActionStatus.class, Action.class, actionStatusRepository::countByActionId);
}
protected void assertActionStatusMessageQuota(final JpaActionStatus actionStatus) {
QuotaHelper.assertAssignmentQuota(actionStatus.getId(), actionStatus.getMessages().size(),
quotaManagement.getMaxMessagesPerActionStatus(), "Message", ActionStatus.class.getSimpleName(), null);
}
}

View File

@@ -0,0 +1,240 @@
/**
* Copyright (c) 2022 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.jpa;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
import org.eclipse.hawkbit.repository.exception.AutoConfirmationAlreadyActiveException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InvalidConfirmationFeedbackException;
import org.eclipse.hawkbit.repository.jpa.builder.JpaActionStatusCreate;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.model.JpaAutoConfirmationStatus;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
import org.eclipse.hawkbit.repository.model.Target;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.annotation.Validated;
/**
* JPA implementation for {@link ConfirmationManagement}.
*
*/
@Transactional(readOnly = true)
@Validated
public class JpaConfirmationManagement extends JpaActionManagement implements ConfirmationManagement {
public static final String CONFIRMATION_CODE_MSG_PREFIX = "Confirmation status code: %d";
private static final Logger LOG = LoggerFactory.getLogger(JpaConfirmationManagement.class);
private final EntityFactory entityFactory;
private final TargetRepository targetRepository;
/**
* Constructor
*/
protected JpaConfirmationManagement(final TargetRepository targetRepository,
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final RepositoryProperties repositoryProperties, final QuotaManagement quotaManagement,
final EntityFactory entityFactory) {
super(actionRepository, actionStatusRepository, quotaManagement, repositoryProperties);
this.targetRepository = targetRepository;
this.entityFactory = entityFactory;
}
@Override
public List<Action> findActiveActionsWaitingConfirmation(final String controllerId) {
return Collections.unmodifiableList(findActiveActionsHavingStatus(controllerId, Status.WAIT_FOR_CONFIRMATION));
}
@Override
@Transactional
public AutoConfirmationStatus activateAutoConfirmation(final String controllerId, final String initiator,
final String remark) {
LOG.trace("'activateAutoConfirmation' was called with values: controllerId={}; initiator={}; remark={}",
controllerId, initiator, remark);
final JpaTarget target = getTargetByControllerIdAndThrowIfNotFound(controllerId);
if (target.getAutoConfirmationStatus() != null) {
LOG.debug("'activateAutoConfirmation' was called for an controller id {} with active auto confirmation.",
controllerId);
throw new AutoConfirmationAlreadyActiveException(controllerId);
}
final JpaAutoConfirmationStatus confirmationStatus = new JpaAutoConfirmationStatus(initiator, remark, target);
target.setAutoConfirmationStatus(confirmationStatus);
final JpaTarget updatedTarget = targetRepository.save(target);
final AutoConfirmationStatus autoConfStatus = updatedTarget.getAutoConfirmationStatus();
if (autoConfStatus == null) {
final String message = String.format("Persisted auto confirmation status is null. "
+ "Cannot proceed with giving confirmations for active actions for device %s with initiator %s.",
controllerId, initiator);
LOG.error("message");
throw new IllegalStateException(message);
}
giveConfirmationForActiveActions(autoConfStatus);
return autoConfStatus;
}
@Override
public Optional<AutoConfirmationStatus> getStatus(final String controllerId) {
return Optional.of(getTargetByControllerIdAndThrowIfNotFound(controllerId)).map(JpaTarget::getAutoConfirmationStatus);
}
@Override
@Transactional
public List<Action> autoConfirmActiveActions(final String controllerId) {
final JpaTarget target = getTargetByControllerIdAndThrowIfNotFound(controllerId);
if (target.getAutoConfirmationStatus() == null) {
// auto-confirmation is not active
return Collections.emptyList();
}
return giveConfirmationForActiveActions(target.getAutoConfirmationStatus());
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Action confirmAction(final long actionId, final Integer code, final Collection<String> deviceMessages) {
LOG.trace("Action with id {} confirm request is triggered.", actionId);
final Action action = getActionAndThrowExceptionIfNotFound(actionId);
assertActionCanAcceptFeedback(action);
final List<String> messages = new ArrayList<>();
if (deviceMessages != null) {
messages.addAll(deviceMessages);
}
messages.add(RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target confirmed action."
+ " Therefore, it will be set to the running state to proceed with the deployment.");
final ActionStatusCreate statusCreate = createConfirmationActionStatus(action.getId(), code, messages)
.status(Status.RUNNING);
return addActionStatus((JpaActionStatusCreate) statusCreate);
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Action denyAction(final long actionId, final Integer code, final Collection<String> deviceMessages) {
LOG.trace("Action with id {} deny request is triggered.", actionId);
final Action action = getActionAndThrowExceptionIfNotFound(actionId);
assertActionCanAcceptFeedback(action);
final List<String> messages = new ArrayList<>();
if (deviceMessages != null) {
messages.addAll(deviceMessages);
}
messages.add(RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target rejected action."
+ " Action will stay in confirmation pending state.");
final ActionStatusCreate statusCreate = createConfirmationActionStatus(action.getId(), code, messages)
.status(Status.WAIT_FOR_CONFIRMATION);
return addActionStatus((JpaActionStatusCreate) statusCreate);
}
private ActionStatusCreate createConfirmationActionStatus(final long actionId, final Integer code,
final Collection<String> messages) {
final ActionStatusCreate statusCreate = entityFactory.actionStatus().create(actionId);
if (!CollectionUtils.isEmpty(messages)) {
statusCreate.messages(messages);
}
if (code != null) {
statusCreate.code(code);
statusCreate.message(String.format(CONFIRMATION_CODE_MSG_PREFIX, code));
}
return statusCreate;
}
private static void assertActionCanAcceptFeedback(final Action action) {
if (!action.isActive()) {
final String msg = String.format(
"Confirming action %s is not possible since the action is not active anymore.", action.getId());
LOG.warn(msg);
throw new InvalidConfirmationFeedbackException(InvalidConfirmationFeedbackException.Reason.ACTION_CLOSED,
msg);
} else if (!action.isWaitingConfirmation()) {
LOG.debug("Action is not waiting for confirmation, deny request.");
final String msg = String.format("Action %s is not waiting for confirmation.", action.getId());
LOG.warn(msg);
throw new InvalidConfirmationFeedbackException(
InvalidConfirmationFeedbackException.Reason.NOT_AWAITING_CONFIRMATION, msg);
}
}
private List<Action> giveConfirmationForActiveActions(final AutoConfirmationStatus autoConfirmationStatus) {
final Target target = autoConfirmationStatus.getTarget();
return findActiveActionsHavingStatus(target.getControllerId(), Status.WAIT_FOR_CONFIRMATION).stream()
.map(action -> autoConfirmAction(action, autoConfirmationStatus)).collect(Collectors.toList());
}
private Action autoConfirmAction(final JpaAction action, final AutoConfirmationStatus autoConfirmationStatus) {
if (!action.isWaitingConfirmation()) {
LOG.debug("Auto-confirming action is not necessary, since action {} is in RUNNING state already.",
action.getId());
return action;
}
final JpaActionStatus actionStatus = (JpaActionStatus) entityFactory.actionStatus().create(action.getId())
.status(Status.RUNNING)
.messages(Collections.singletonList(autoConfirmationStatus.constructActionMessage())).build();
LOG.debug(
"Automatically confirm actionId '{}' due to active auto-confirmation initiated by '{}' and rollouts system user '{}'",
action.getId(), autoConfirmationStatus.getInitiator(), autoConfirmationStatus.getCreatedBy());
// do not make use of
// org.eclipse.hawkbit.repository.jpa.JpaActionManagement.handleAddUpdateActionStatus
// to bypass the quota check. Otherwise the action will not be confirmed in case
// of exceeded action status quota.
action.setStatus(Status.RUNNING);
actionStatus.setAction(action);
actionStatusRepository.save(actionStatus);
return actionRepository.save(action);
}
@Override
@Transactional
public void deactivateAutoConfirmation(String controllerId) {
LOG.debug("Deactivate auto confirmation for controllerId '{}'", controllerId);
final JpaTarget target = getTargetByControllerIdAndThrowIfNotFound(controllerId);
target.setAutoConfirmationStatus(null);
targetRepository.save(target);
}
private JpaTarget getTargetByControllerIdAndThrowIfNotFound(final String controllerId) {
return targetRepository.findOne(TargetSpecifications.hasControllerId(controllerId))
.orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId));
}
@Override
protected void onActionStatusUpdate(final Status updatedActionStatus, final JpaAction action) {
if (updatedActionStatus == Status.RUNNING && action.isActive()) {
action.setStatus(Status.RUNNING);
}
}
}

View File

@@ -8,7 +8,6 @@
*/
package org.eclipse.hawkbit.repository.jpa;
import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY;
import static org.eclipse.hawkbit.repository.model.Action.Status.DOWNLOADED;
import static org.eclipse.hawkbit.repository.model.Action.Status.FINISHED;
import static org.eclipse.hawkbit.repository.model.Target.CONTROLLER_ATTRIBUTE_KEY_SIZE;
@@ -39,6 +38,7 @@ import javax.persistence.criteria.Root;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
@@ -72,6 +72,7 @@ import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
@@ -125,12 +126,6 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
@Autowired
private SoftwareModuleRepository softwareModuleRepository;
@Autowired
private ActionStatusRepository actionStatusRepository;
@Autowired
private QuotaManagement quotaManagement;
@Autowired
private TenantConfigurationManagement tenantConfigurationManagement;
@@ -155,9 +150,13 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
@Autowired
private TenantAware tenantAware;
@Autowired
private ConfirmationManagement confirmationManagement;
public JpaControllerManagement(final ScheduledExecutorService executorService,
final RepositoryProperties repositoryProperties, final ActionRepository actionRepository) {
super(actionRepository, repositoryProperties);
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final RepositoryProperties repositoryProperties) {
super(actionRepository, actionStatusRepository, quotaManagement, repositoryProperties);
if (!repositoryProperties.isEagerPollPersistence()) {
executorService.scheduleWithFixedDelay(this::flushUpdateQueue,
@@ -565,11 +564,6 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
return action;
}
private void assertActionStatusMessageQuota(final JpaActionStatus actionStatus) {
QuotaHelper.assertAssignmentQuota(actionStatus.getId(), actionStatus.getMessages().size(),
quotaManagement.getMaxMessagesPerActionStatus(), "Message", ActionStatus.class.getSimpleName(), null);
}
private void handleFinishedCancelation(final JpaActionStatus actionStatus, final JpaAction action) {
// in case of successful cancellation we also report the success at
// the canceled action itself.
@@ -582,88 +576,41 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Action addUpdateActionStatus(final ActionStatusCreate c) {
final JpaActionStatusCreate create = (JpaActionStatusCreate) c;
final JpaAction action = getActionAndThrowExceptionIfNotFound(create.getActionId());
final JpaActionStatus actionStatus = create.build();
if (isUpdatingActionStatusAllowed(action, actionStatus)) {
return handleAddUpdateActionStatus(actionStatus, action);
}
LOG.debug("Update of actionStatus {} for action {} not possible since action not active anymore.",
actionStatus.getStatus(), action.getId());
return action;
public Action addUpdateActionStatus(final ActionStatusCreate statusCreate) {
return addActionStatus((JpaActionStatusCreate) statusCreate);
}
/**
* ActionStatus updates are allowed mainly if the action is active. If the
* action is not active we accept further status updates if permitted so by
* repository configuration. In this case, only the values: Status.ERROR and
* Status.FINISHED are allowed. In the case of a DOWNLOAD_ONLY action, we accept
* status updates only once.
*/
private boolean isUpdatingActionStatusAllowed(final JpaAction action, final JpaActionStatus actionStatus) {
final boolean isIntermediateFeedback = (FINISHED != actionStatus.getStatus())
&& (Status.ERROR != actionStatus.getStatus());
final boolean isAllowedByRepositoryConfiguration = !repositoryProperties.isRejectActionStatusForClosedAction()
&& isIntermediateFeedback;
final boolean isAllowedForDownloadOnlyActions = isDownloadOnly(action) && !isIntermediateFeedback;
return action.isActive() || isAllowedByRepositoryConfiguration || isAllowedForDownloadOnlyActions;
}
private static boolean isDownloadOnly(final JpaAction action) {
return DOWNLOAD_ONLY == action.getActionType();
}
/**
* Sets {@link TargetUpdateStatus} based on given {@link ActionStatus}.
*/
private Action handleAddUpdateActionStatus(final JpaActionStatus actionStatus, final JpaAction action) {
String controllerId = null;
LOG.debug("handleAddUpdateActionStatus for action {}", action.getId());
// information status entry - check for a potential DOS attack
assertActionStatusQuota(action);
assertActionStatusMessageQuota(actionStatus);
switch (actionStatus.getStatus()) {
@Override
protected void onActionStatusUpdate(final Action.Status updatedActionStatus, final JpaAction action) {
switch (updatedActionStatus) {
case ERROR:
final JpaTarget target = (JpaTarget) action.getTarget();
target.setUpdateStatus(TargetUpdateStatus.ERROR);
handleErrorOnAction(action, target);
break;
case FINISHED:
controllerId = handleFinishedAndStoreInTargetStatus(action);
handleFinishedAndStoreInTargetStatus(action).ifPresent(this::requestControllerAttributes);
break;
case DOWNLOADED:
controllerId = handleDownloadedActionStatus(action);
handleDownloadedActionStatus(action).ifPresent(this::requestControllerAttributes);
break;
default:
break;
}
actionStatus.setAction(action);
actionStatusRepository.save(actionStatus);
action.setLastActionStatusCode(actionStatus.getCode().orElse(null));
final Action savedAction = actionRepository.save(action);
if (controllerId != null) {
requestControllerAttributes(controllerId);
}
return savedAction;
}
private String handleDownloadedActionStatus(final JpaAction action) {
/**
* Handles the case where the {@link Action.Status#DOWNLOADED} status is
* reported by the device. In case the update is finished, a controllerId will
* be returned to trigger a request for attributes.
*
* @param action
* updated action
* @return a present controllerId in case the attributes needs to be requested.
*/
private Optional<String> handleDownloadedActionStatus(final JpaAction action) {
if (!isDownloadOnly(action)) {
return null;
return Optional.empty();
}
final JpaTarget target = (JpaTarget) action.getTarget();
@@ -672,7 +619,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
target.setUpdateStatus(TargetUpdateStatus.IN_SYNC);
targetRepository.save(target);
return target.getControllerId();
return Optional.of(target.getControllerId());
}
private void requestControllerAttributes(final String controllerId) {
@@ -695,12 +642,16 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
targetRepository.save(mergedTarget);
}
private void assertActionStatusQuota(final JpaAction action) {
QuotaHelper.assertAssignmentQuota(action.getId(), 1, quotaManagement.getMaxStatusEntriesPerAction(),
ActionStatus.class, Action.class, actionStatusRepository::countByActionId);
}
private String handleFinishedAndStoreInTargetStatus(final JpaAction action) {
/**
* Handles the case where the {@link Action.Status#FINISHED} status is
* reported by the device. In case the update is finished, a controllerId will
* be returned to trigger a request for attributes.
*
* @param action
* updated action
* @return a present controllerId in case the attributes needs to be requested.
*/
private Optional<String> handleFinishedAndStoreInTargetStatus(final JpaAction action) {
final JpaTarget target = (JpaTarget) action.getTarget();
action.setActive(false);
action.setStatus(Status.FINISHED);
@@ -727,7 +678,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
targetRepository.save(target);
entityManager.detach(ds);
return target.getControllerId();
return Optional.of(target.getControllerId());
}
@Override
@@ -884,11 +835,6 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
return actionStatusRepository.save(statusMessage);
}
private JpaAction getActionAndThrowExceptionIfNotFound(final Long actionId) {
return actionRepository.findById(actionId)
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
}
@Override
public Optional<Target> getByControllerId(final String controllerId) {
return targetRepository.findOne(TargetSpecifications.hasControllerId(controllerId)).map(Target.class::cast);
@@ -1076,6 +1022,17 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
}
}
@Override
public AutoConfirmationStatus activateAutoConfirmation(final String controllerId, final String initiator,
final String remark) {
return confirmationManagement.activateAutoConfirmation(controllerId, initiator, remark);
}
@Override
public void deactivateAutoConfirmation(final String controllerId) {
confirmationManagement.deactivateAutoConfirmation(controllerId);
}
private void cancelAssignDistributionSetEvent(final Action action) {
afterCommit.afterCommit(() -> eventPublisherHolder.getEventPublisher().publishEvent(
new CancelTargetAssignmentEvent(action, eventPublisherHolder.getApplicationId())));

View File

@@ -16,6 +16,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -61,7 +62,6 @@ import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper;
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
import org.eclipse.hawkbit.repository.jpa.utils.TenantConfigHelper;
import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
@@ -81,6 +81,7 @@ import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.utils.TenantConfigHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.ConcurrencyFailureException;
@@ -138,14 +139,12 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
private final DistributionSetManagement distributionSetManagement;
private final DistributionSetRepository distributionSetRepository;
private final TargetRepository targetRepository;
private final ActionStatusRepository actionStatusRepository;
private final AuditorAware<String> auditorProvider;
private final VirtualPropertyReplacer virtualPropertyReplacer;
private final PlatformTransactionManager txManager;
private final OnlineDsAssignmentStrategy onlineDsAssignmentStrategy;
private final OfflineDsAssignmentStrategy offlineDsAssignmentStrategy;
private final TenantConfigurationManagement tenantConfigurationManagement;
private final QuotaManagement quotaManagement;
private final SystemSecurityContext systemSecurityContext;
private final TenantAware tenantAware;
private final Database database;
@@ -160,26 +159,25 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
final TenantConfigurationManagement tenantConfigurationManagement, final QuotaManagement quotaManagement,
final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware, final Database database,
final RepositoryProperties repositoryProperties) {
super(actionRepository, repositoryProperties);
super(actionRepository, actionStatusRepository, quotaManagement, repositoryProperties);
this.entityManager = entityManager;
this.distributionSetRepository = distributionSetRepository;
this.distributionSetManagement = distributionSetManagement;
this.targetRepository = targetRepository;
this.actionStatusRepository = actionStatusRepository;
this.auditorProvider = auditorProvider;
this.virtualPropertyReplacer = virtualPropertyReplacer;
this.txManager = txManager;
onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisherHolder,
actionRepository, actionStatusRepository, quotaManagement, this::isMultiAssignmentsEnabled);
actionRepository, actionStatusRepository, quotaManagement, this::isMultiAssignmentsEnabled,
this::isConfirmationFlowEnabled);
offlineDsAssignmentStrategy = new OfflineDsAssignmentStrategy(targetRepository, afterCommit,
eventPublisherHolder, actionRepository, actionStatusRepository, quotaManagement,
this::isMultiAssignmentsEnabled);
this::isMultiAssignmentsEnabled, this::isConfirmationFlowEnabled);
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.quotaManagement = quotaManagement;
this.systemSecurityContext = systemSecurityContext;
this.tenantAware = tenantAware;
this.database = database;
retryTemplate = createRetryTemplate();
this.retryTemplate = createRetryTemplate();
}
@Override
@@ -393,7 +391,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
targetEntitiesIdsChunks.forEach(this::cancelInactiveScheduledActionsForTargets);
setAssignedDistributionSetAndTargetUpdateStatus(assignmentStrategy, distributionSetEntity,
targetEntitiesIdsChunks);
final List<JpaAction> assignedActions = createActions(initiatedBy, targetsWithActionType, targetEntities,
final Map<TargetWithActionType, JpaAction> assignedActions = createActions(initiatedBy, targetsWithActionType, targetEntities,
assignmentStrategy, distributionSetEntity);
// create initial action status when action is created so we remember
// the initial running status because we will change the status
@@ -402,7 +400,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
createActionsStatus(assignedActions, assignmentStrategy, actionMessage);
detachEntitiesAndSendTargetUpdatedEvents(distributionSetEntity, targetEntities, assignmentStrategy);
return assignedActions;
return new ArrayList<>(assignedActions.values());
}
/**
@@ -473,20 +471,62 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
assignmentStrategy.setAssignedDistributionSetAndTargetStatus(set, targetIdsChunks, currentUser);
}
private List<JpaAction> createActions(final String initiatedBy,
private Map<TargetWithActionType, JpaAction> createActions(final String initiatedBy,
final Collection<TargetWithActionType> targetsWithActionType, final List<JpaTarget> targets,
final AbstractDsAssignmentStrategy assignmentStrategy, final JpaDistributionSet set) {
return targetsWithActionType.stream()
.map(twt -> assignmentStrategy.createTargetAction(initiatedBy, twt, targets, set))
.filter(Objects::nonNull).map(actionRepository::save).collect(Collectors.toList());
final Map<TargetWithActionType, JpaAction> persistedActions = new LinkedHashMap<>();
for (final TargetWithActionType twt : targetsWithActionType) {
final JpaAction targetAction = assignmentStrategy.createTargetAction(initiatedBy, twt, targets, set);
if (targetAction != null) {
persistedActions.put(twt, actionRepository.save(targetAction));
}
}
return persistedActions;
}
private void createActionsStatus(final Collection<JpaAction> actions,
private void createActionsStatus(final Map<TargetWithActionType, JpaAction> actions,
final AbstractDsAssignmentStrategy assignmentStrategy, final String actionMessage) {
actionStatusRepository
.saveAll(actions.stream().map(action -> assignmentStrategy.createActionStatus(action, actionMessage))
.collect(Collectors.toList()));
actionStatusRepository.saveAll(actions.entrySet().stream().map(entry -> {
final JpaAction action = entry.getValue();
final JpaActionStatus actionStatus = assignmentStrategy.createActionStatus(action, actionMessage);
verifyAndAddConfirmationStatus(action, actionStatus, entry.getKey().isConfirmationRequired());
return actionStatus;
}).collect(Collectors.toList()));
}
private void setInitialActionStatusOfRolloutGroup(final List<JpaAction> actions) {
final List<JpaActionStatus> statusList = new ArrayList<>();
for (final JpaAction action : actions) {
final JpaActionStatus actionStatus = onlineDsAssignmentStrategy.createActionStatus(action, null);
verifyAndAddConfirmationStatus(action, actionStatus, action.getRolloutGroup().isConfirmationRequired());
statusList.add(actionStatus);
}
actionStatusRepository.saveAll(statusList);
}
private void verifyAndAddConfirmationStatus(final JpaAction action, final JpaActionStatus actionStatus,
final boolean isConfirmationRequired) {
if (actionStatus.getStatus() == Status.WAIT_FOR_CONFIRMATION) {
if (action.getStatus().equals(Status.RUNNING)) {
// action is in RUNNING state only if it's confirmed during assignment already
if (!isConfirmationRequired) {
// confirmation given on assignment dialog
actionStatus.addMessage(
String.format("Assignment confirmed by initiator [%s].", action.getInitiatedBy()));
} else if (action.getTarget().getAutoConfirmationStatus() != null) {
// auto-confirmation is configured
actionStatus.addMessage(action.getTarget().getAutoConfirmationStatus().constructActionMessage());
} else {
throw new IllegalStateException("Action in RUNNING state without given confirmation.");
}
} else {
actionStatus
.addMessage("Waiting for the confirmation by the device before processing with the deployment");
}
}
}
private void detachEntitiesAndSendTargetUpdatedEvents(final JpaDistributionSet set, final List<JpaTarget> targets,
@@ -643,15 +683,15 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
if (!isMultiAssignmentsEnabled()) {
closeOrCancelOpenDeviceActions(actions);
}
final List<JpaAction> savedActions = activateActions(actions);
setInitialActionStatus(savedActions);
final List<JpaAction> savedActions = activateActionsOfRolloutGroup(actions);
setInitialActionStatusOfRolloutGroup(savedActions);
setAssignmentOnTargets(savedActions);
return Collections.unmodifiableList(savedActions);
}
private void closeOrCancelOpenDeviceActions(final List<JpaAction> actions) {
private void closeOrCancelOpenDeviceActions(final List<JpaAction> actions){
final List<Long> targetIds = actions.stream().map(JpaAction::getTarget).map(Target::getId)
.collect(Collectors.toList());
.collect(Collectors.toList());
if (isActionsAutocloseEnabled()) {
onlineDsAssignmentStrategy.closeObsoleteUpdateActions(targetIds);
} else {
@@ -659,9 +699,15 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
}
}
private List<JpaAction> activateActions(final List<JpaAction> actions) {
private List<JpaAction> activateActionsOfRolloutGroup(final List<JpaAction> actions) {
actions.forEach(action -> {
action.setActive(true);
final boolean confirmationRequired = action.getRolloutGroup().isConfirmationRequired()
&& action.getTarget().getAutoConfirmationStatus() == null;
if (isConfirmationFlowEnabled() && confirmationRequired) {
action.setStatus(Status.WAIT_FOR_CONFIRMATION);
return;
}
action.setStatus(Status.RUNNING);
});
return actionRepository.saveAll(actions);
@@ -678,14 +724,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
targetRepository.saveAll(assignedDsTargets);
}
private void setInitialActionStatus(final List<JpaAction> actions) {
final List<JpaActionStatus> statusList = new ArrayList<>();
for (final JpaAction action : actions) {
statusList.add(onlineDsAssignmentStrategy.createActionStatus(action, null));
}
actionStatusRepository.saveAll(statusList);
}
private void setSkipActionStatus(final JpaAction action) {
final JpaActionStatus actionStatus = new JpaActionStatus();
actionStatus.setAction(action);
@@ -745,7 +783,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
public List<Action> findActiveActionsWithHighestWeight(final String controllerId, final int maxActionCount) {
return findActiveActionsWithHighestWeightConsideringDefault(controllerId, maxActionCount);
}
@Override
public int getWeightConsideringDefault(final Action action) {
return super.getWeightConsideringDefault(action);
@@ -967,6 +1004,11 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
.isMultiAssignmentsEnabled();
}
private boolean isConfirmationFlowEnabled() {
return TenantConfigHelper.usingContext(systemSecurityContext, tenantConfigurationManagement)
.isConfirmationFlowEnabled();
}
private <T extends Serializable> T getConfigValue(final String key, final Class<T> valueType) {
return systemSecurityContext
.runAsSystem(() -> tenantConfigurationManagement.getConfigurationValue(key, valueType).getValue());
@@ -1002,5 +1044,4 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
});
}
}
}

View File

@@ -71,6 +71,7 @@ import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.utils.TenantConfigHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -193,10 +194,11 @@ public class JpaRolloutManagement implements RolloutManagement {
@Transactional
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Rollout create(final RolloutCreate rollout, final int amountGroup, final RolloutGroupConditions conditions) {
public Rollout create(final RolloutCreate rollout, final int amountGroup, final boolean confirmationRequired,
final RolloutGroupConditions conditions) {
RolloutHelper.verifyRolloutGroupParameter(amountGroup, quotaManagement);
final JpaRollout savedRollout = createRollout((JpaRollout) rollout.build());
return createRolloutGroups(amountGroup, conditions, savedRollout);
return createRolloutGroups(amountGroup, conditions, savedRollout, confirmationRequired);
}
@Override
@@ -222,7 +224,7 @@ public class JpaRolloutManagement implements RolloutManagement {
}
private Rollout createRolloutGroups(final int amountOfGroups, final RolloutGroupConditions conditions,
final JpaRollout rollout) {
final JpaRollout rollout, final boolean isConfirmationRequired) {
RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING);
RolloutHelper.verifyRolloutGroupConditions(conditions);
@@ -241,6 +243,7 @@ public class JpaRolloutManagement implements RolloutManagement {
group.setRollout(savedRollout);
group.setParent(lastSavedGroup);
group.setStatus(RolloutGroupStatus.CREATING);
group.setConfirmationRequired(isConfirmationRequired);
addSuccessAndErrorConditionsAndActions(group, conditions);
@@ -283,6 +286,7 @@ public class JpaRolloutManagement implements RolloutManagement {
group.setRollout(savedRollout);
group.setParent(lastSavedGroup);
group.setStatus(RolloutGroupStatus.CREATING);
group.setConfirmationRequired(srcGroup.isConfirmationRequired());
group.setTargetPercentage(srcGroup.getTargetPercentage());
if (srcGroup.getTargetFilterQuery() != null) {

View File

@@ -43,6 +43,7 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.utils.TenantConfigHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.ConcurrencyFailureException;
@@ -249,6 +250,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
// set the new query
targetFilterQuery.setQuery(query);
});
update.getConfirmationRequired().ifPresent(targetFilterQuery::setConfirmationRequired);
return targetFilterQueryRepository.save(targetFilterQuery);
}
@@ -263,6 +265,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
targetFilterQuery.setAutoAssignActionType(null);
targetFilterQuery.setAutoAssignWeight(null);
targetFilterQuery.setAutoAssignInitiatedBy(null);
targetFilterQuery.setConfirmationRequired(false);
} else {
WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(update);
// we cannot be sure that the quota was enforced at creation time
@@ -277,10 +280,18 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
targetFilterQuery.setAutoAssignInitiatedBy(tenantAware.getCurrentUsername());
targetFilterQuery.setAutoAssignActionType(sanitizeAutoAssignActionType(update.getActionType()));
targetFilterQuery.setAutoAssignWeight(update.getWeight());
final boolean confirmationRequired = update.isConfirmationRequired() == null ? isConfirmationFlowEnabled()
: update.isConfirmationRequired();
targetFilterQuery.setConfirmationRequired(confirmationRequired);
}
return targetFilterQueryRepository.save(targetFilterQuery);
}
private boolean isConfirmationFlowEnabled() {
return TenantConfigHelper.usingContext(systemSecurityContext, tenantConfigurationManagement)
.isConfirmationFlowEnabled();
}
private static void verifyDistributionSetAndThrowExceptionIfDeleted(final DistributionSet distributionSet) {
if (distributionSet.isDeleted()) {
throw new EntityNotFoundException(DistributionSet.class, distributionSet.getId());

View File

@@ -40,12 +40,14 @@ import org.eclipse.hawkbit.repository.builder.TargetUpdate;
import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
import org.eclipse.hawkbit.repository.exception.AutoConfirmationAlreadyActiveException;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetCreate;
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetUpdate;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.model.JpaAutoConfirmationStatus;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata_;
@@ -57,6 +59,7 @@ import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.MetaData;

View File

@@ -45,9 +45,10 @@ public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
OfflineDsAssignmentStrategy(final TargetRepository targetRepository,
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig) {
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig,
final BooleanSupplier confirmationFlowConfig) {
super(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository,
quotaManagement, multiAssignmentsConfig);
quotaManagement, multiAssignmentsConfig, confirmationFlowConfig);
}
@Override

View File

@@ -48,9 +48,10 @@ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
OnlineDsAssignmentStrategy(final TargetRepository targetRepository,
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig) {
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig,
final BooleanSupplier confirmationFlowConfig) {
super(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository,
quotaManagement, multiAssignmentsConfig);
quotaManagement, multiAssignmentsConfig, confirmationFlowConfig);
}
@Override
@@ -130,15 +131,28 @@ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
final List<JpaTarget> targets, final JpaDistributionSet set) {
final JpaAction result = super.createTargetAction(initiatedBy, targetWithActionType, targets, set);
if (result != null) {
result.setStatus(Status.RUNNING);
final boolean confirmationRequired = targetWithActionType.isConfirmationRequired()
&& result.getTarget().getAutoConfirmationStatus() == null;
if (isConfirmationFlowEnabled() && confirmationRequired) {
result.setStatus(Status.WAIT_FOR_CONFIRMATION);
} else {
result.setStatus(Status.RUNNING);
}
}
return result;
}
/**
* Will be called to create the initial action status for an action
*/
@Override
public JpaActionStatus createActionStatus(final JpaAction action, final String actionMessage) {
final JpaActionStatus result = super.createActionStatus(action, actionMessage);
result.setStatus(Status.RUNNING);
if (isConfirmationFlowEnabled()) {
result.setStatus(Status.WAIT_FOR_CONFIRMATION);
} else {
result.setStatus(Status.RUNNING);
}
return result;
}

View File

@@ -22,6 +22,7 @@ import org.eclipse.hawkbit.repository.ArtifactEncryptionSecretsStore;
import org.eclipse.hawkbit.repository.ArtifactEncryptionService;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.BaseRepositoryTypeProvider;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
@@ -742,6 +743,16 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
quotaManagement, systemSecurityContext, tenantAware, properties.getDatabase(), repositoryProperties);
}
@Bean
@ConditionalOnMissingBean
ConfirmationManagement confirmationManagement(final TargetRepository targetRepository,
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final RepositoryProperties repositoryProperties, final QuotaManagement quotaManagement,
final EntityFactory entityFactory) {
return new JpaConfirmationManagement(targetRepository, actionRepository, actionStatusRepository,
repositoryProperties, quotaManagement, entityFactory);
}
/**
* {@link JpaControllerManagement} bean.
*
@@ -750,8 +761,10 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
@Bean
@ConditionalOnMissingBean
ControllerManagement controllerManagement(final ScheduledExecutorService executorService,
final RepositoryProperties repositoryProperties, final ActionRepository actionRepository) {
return new JpaControllerManagement(executorService, repositoryProperties, actionRepository);
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final RepositoryProperties repositoryProperties) {
return new JpaControllerManagement(executorService, actionRepository, actionStatusRepository, quotaManagement,
repositoryProperties);
}
@Bean

View File

@@ -167,7 +167,7 @@ public abstract class AbstractAutoAssignExecutor implements AutoAssignExecutor {
.map(controllerId -> DeploymentManagement
.deploymentRequest(controllerId, filterQuery.getAutoAssignDistributionSet().getId())
.setActionType(autoAssignActionType).setWeight(filterQuery.getAutoAssignWeight().orElse(null))
.build())
.setConfirmationRequired(filterQuery.isConfirmationRequired()).build())
.collect(Collectors.toList());
}

View File

@@ -34,6 +34,7 @@ public class JpaRolloutGroupCreate extends AbstractRolloutGroupCreate<RolloutGro
if (conditions != null) {
addSuccessAndErrorConditionsAndActions(group, conditions);
}
group.setConfirmationRequired(confirmationRequired);
return group;
}

View File

@@ -31,11 +31,10 @@ public class JpaTargetFilterQueryCreate extends AbstractTargetFilterQueryUpdateC
@Override
public JpaTargetFilterQuery build() {
return new JpaTargetFilterQuery(name, query,
getAutoAssignDistributionSetId().map(distributionSetManagement::getValidAndComplete).orElse(null),
getAutoAssignActionType().filter(JpaTargetFilterQueryCreate::isAutoAssignActionTypeValid).orElse(null),
weight);
weight, getConfirmationRequired().orElse(false));
}
private static boolean isAutoAssignActionTypeValid(final ActionType actionType) {

View File

@@ -108,7 +108,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
@ConversionValue(objectValue = "DOWNLOAD", dataValue = "7"),
@ConversionValue(objectValue = "SCHEDULED", dataValue = "8"),
@ConversionValue(objectValue = "CANCEL_REJECTED", dataValue = "9"),
@ConversionValue(objectValue = "DOWNLOADED", dataValue = "10") })
@ConversionValue(objectValue = "DOWNLOADED", dataValue = "10"),
@ConversionValue(objectValue = "WAIT_FOR_CONFIRMATION", dataValue = "11")})
@Convert("status")
@NotNull
private Status status;
@@ -236,7 +237,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
public String toString() {
return "JpaAction [distributionSet=" + distributionSet.getId() + ", version=" + getOptLockRevision() + ", id="
+ getId() + ", actionType=" + getActionType() + ", weight=" + getWeight() + ", isActive=" + isActive()
+ ", createdAt=" + getCreatedAt() + ", lastModifiedAt=" + getLastModifiedAt() + "]";
+ ", createdAt=" + getCreatedAt() + ", lastModifiedAt=" + getLastModifiedAt() + ", status="
+ getStatus().name() + "]";
}
@Override
@@ -387,4 +389,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
public void setLastActionStatusCode(final Integer lastActionStatusCode) {
this.lastActionStatusCode = lastActionStatusCode;
}
public boolean isWaitingConfirmation() {
return status == Status.WAIT_FOR_CONFIRMATION;
}
}

View File

@@ -74,7 +74,8 @@ public class JpaActionStatus extends AbstractJpaTenantAwareBaseEntity implements
@ConversionValue(objectValue = "DOWNLOAD", dataValue = "7"),
@ConversionValue(objectValue = "SCHEDULED", dataValue = "8"),
@ConversionValue(objectValue = "CANCEL_REJECTED", dataValue = "9"),
@ConversionValue(objectValue = "DOWNLOADED", dataValue = "10")})
@ConversionValue(objectValue = "DOWNLOADED", dataValue = "10"),
@ConversionValue(objectValue = "WAIT_FOR_CONFIRMATION", dataValue = "11")})
@Convert("status")
@NotNull
private Status status;

View File

@@ -0,0 +1,92 @@
/**
* Copyright (c) 2022 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.jpa.model;
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.Target;
import org.springframework.util.StringUtils;
import javax.persistence.Column;
import javax.persistence.ConstraintMode;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ForeignKey;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.validation.constraints.Size;
@Entity
@Table(name = "sp_target_conf_status")
public class JpaAutoConfirmationStatus extends AbstractJpaTenantAwareBaseEntity implements AutoConfirmationStatus {
@OneToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "target_id", nullable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_auto_conf"))
private JpaTarget target;
@Column(name = "initiator", length = USERNAME_FIELD_LENGTH)
@Size(max = USERNAME_FIELD_LENGTH)
private String initiator;
@Column(name = "remark", length = NamedEntity.DESCRIPTION_MAX_SIZE)
@Size(max = NamedEntity.DESCRIPTION_MAX_SIZE)
private String remark;
/**
* Default constructor needed for JPA entities.
*/
public JpaAutoConfirmationStatus() {
// Default constructor needed for JPA entities.
}
public JpaAutoConfirmationStatus(final String initiator, final String remark, final Target target) {
this.target = (JpaTarget) target;
this.initiator = StringUtils.isEmpty(initiator) ? null : initiator;
this.remark = StringUtils.isEmpty(remark) ? null : remark;
}
@Override
public Target getTarget() {
return target;
}
@Override
public String getInitiator() {
return initiator;
}
@Override
public long getActivatedAt() {
return getCreatedAt();
}
@Override
public String getRemark() {
return remark;
}
@Override
public String constructActionMessage() {
final String remarkMessage = StringUtils.hasText(remark) ? remark : "n/a";
final String formattedInitiator = StringUtils.hasText(initiator) ? initiator : "n/a";
final String createdByRolloutsUser = StringUtils.hasText(getCreatedBy()) ? getCreatedBy() : "n/a";
return String.format("Assignment automatically confirmed by initiator '%s'. %n%n" //
+ "Auto confirmation activated by system user: '%s' %n%n" //
+ "Remark: %s", formattedInitiator, createdByRolloutsUser, remarkMessage);
}
@Override
public String toString() {
return "AutoConfirmationStatus [id=" + getId() + ", target=" + target.getControllerId() + ", initiator="
+ initiator + ", bosch_user=" + getCreatedBy() + ", activatedAt=" + getCreatedAt() + ", remark="
+ remark + "]";
}
}

View File

@@ -117,6 +117,9 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr
@Column(name = "target_percentage")
private float targetPercentage = 100;
@Column(name = "confirmation_required")
private boolean confirmationRequired;
@Transient
private transient TotalTargetCountStatus totalTargetCountStatus;
@@ -251,6 +254,15 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr
return targetPercentage;
}
public void setConfirmationRequired(final boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
}
@Override
public boolean isConfirmationRequired() {
return confirmationRequired;
}
public void setTargetPercentage(final float targetPercentage) {
this.targetPercentage = targetPercentage;
}
@@ -279,7 +291,8 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr
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() + "]";
+ ", errorExp=" + errorConditionExp + ", getName()=" + getName() + ", getId()=" + getId()
+ ", isConfirmationRequired()=" + isConfirmationRequired() + "]";
}
@Override

View File

@@ -36,6 +36,8 @@ import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.MapKeyColumn;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.validation.constraints.NotNull;
@@ -49,6 +51,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityChecker;
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.PollStatus;
import org.eclipse.hawkbit.repository.model.Target;
@@ -177,6 +180,10 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
@OneToMany(mappedBy = "target", fetch = FetchType.LAZY, targetEntity = JpaTargetMetadata.class)
private List<TargetMetadata> metadata;
@OneToOne(fetch = FetchType.LAZY, mappedBy = "target", orphanRemoval = true)
@PrimaryKeyJoinColumn
private JpaAutoConfirmationStatus autoConfirmationStatus;
/**
* Constructor.
*
@@ -372,6 +379,15 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
});
}
@Override
public AutoConfirmationStatus getAutoConfirmationStatus() {
return autoConfirmationStatus;
}
public void setAutoConfirmationStatus(final JpaAutoConfirmationStatus autoConfirmationStatus) {
this.autoConfirmationStatus = autoConfirmationStatus;
}
@Override
public Long getLastTargetQuery() {
return lastTargetQuery;

View File

@@ -80,6 +80,9 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
@Column(name = "auto_assign_initiated_by", nullable = true, length = USERNAME_FIELD_LENGTH)
private String autoAssignInitiatedBy;
@Column(name = "confirmation_required")
private boolean confirmationRequired;
public JpaTargetFilterQuery() {
// Default constructor for JPA.
}
@@ -97,14 +100,17 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
* of the {@link TargetFilterQuery}.
* @param autoAssignWeight
* of the {@link TargetFilterQuery}.
* @param confirmationRequired
* of the {@link TargetFilterQuery}.
*/
public JpaTargetFilterQuery(final String name, final String query, final DistributionSet autoAssignDistributionSet,
final ActionType autoAssignActionType, final Integer autoAssignWeight) {
final ActionType autoAssignActionType, final Integer autoAssignWeight, final boolean confirmationRequired) {
this.name = name;
this.query = query;
this.autoAssignDistributionSet = (JpaDistributionSet) autoAssignDistributionSet;
this.autoAssignActionType = autoAssignActionType;
this.autoAssignWeight = autoAssignWeight;
this.confirmationRequired = confirmationRequired;
}
@Override
@@ -160,6 +166,15 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
this.autoAssignInitiatedBy = autoAssignInitiatedBy;
}
@Override
public boolean isConfirmationRequired() {
return confirmationRequired;
}
public void setConfirmationRequired(final boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
}
@Override
public void fireCreateEvent(final DescriptorEvent descriptorEvent) {
EventPublisherHolder.getInstance().getEventPublisher().publishEvent(
@@ -177,4 +192,5 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
EventPublisherHolder.getInstance().getEventPublisher().publishEvent(new TargetFilterQueryDeletedEvent(
getTenant(), getId(), getClass(), EventPublisherHolder.getInstance().getApplicationId()));
}
}

View File

@@ -18,6 +18,7 @@ import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryCreate;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.utils.TenantConfigHelper;
/**
* Utility class to handle weight validation in Rollout, Auto Assignments, and

View File

@@ -0,0 +1,22 @@
ALTER TABLE sp_rolloutgroup ADD COLUMN confirmation_required BOOLEAN;
UPDATE sp_rolloutgroup SET confirmation_required = 0;
ALTER TABLE sp_target_filter_query ADD COLUMN confirmation_required BOOLEAN;
UPDATE sp_target_filter_query SET confirmation_required = 0;
CREATE TABLE sp_target_conf_status
(
id BIGINT GENERATED always AS IDENTITY NOT NULL,
target_id bigint not null,
initiator VARCHAR(64),
remark VARCHAR(512),
created_at BIGINT,
created_by VARCHAR(64),
last_modified_at BIGINT,
last_modified_by VARCHAR(64),
optlock_revision BIGINT,
tenant VARCHAR(40) not null,
primary key (id)
);
ALTER TABLE sp_target_conf_status
ADD CONSTRAINT fk_target_auto_conf FOREIGN KEY (target_id) REFERENCES sp_target (id) ON DELETE CASCADE;

View File

@@ -0,0 +1,22 @@
ALTER TABLE sp_rolloutgroup ADD column confirmation_required BOOLEAN;
UPDATE sp_rolloutgroup SET confirmation_required = 0;
ALTER TABLE sp_target_filter_query ADD column confirmation_required BOOLEAN;
UPDATE sp_target_filter_query SET confirmation_required = 0;
create table sp_target_conf_status
(
id bigint not null auto_increment,
target_id bigint not null,
initiator varchar(64),
remark VARCHAR(512),
created_at bigint,
created_by varchar(64),
last_modified_at bigint,
last_modified_by varchar(64),
optlock_revision bigint,
tenant varchar(40) not null,
primary key (id)
);
ALTER TABLE sp_target_conf_status
ADD CONSTRAINT fk_target_auto_conf FOREIGN KEY (target_id) REFERENCES sp_target (id) ON DELETE CASCADE;

View File

@@ -0,0 +1,22 @@
ALTER TABLE sp_rolloutgroup ADD column confirmation_required BOOLEAN;
UPDATE sp_rolloutgroup SET confirmation_required = 0;
ALTER TABLE sp_target_filter_query ADD column confirmation_required BOOLEAN;
UPDATE sp_target_filter_query SET confirmation_required = 0;
create table sp_target_conf_status
(
id bigint not null auto_increment,
target_id bigint not null,
initiator varchar(64),
remark varchar(512),
created_at bigint,
created_by varchar(64),
last_modified_at bigint,
last_modified_by varchar(64),
optlock_revision bigint,
tenant varchar(40) not null,
primary key (id)
);
ALTER TABLE sp_target_conf_status
ADD CONSTRAINT fk_target_auto_conf FOREIGN KEY (target_id) REFERENCES sp_target (id) ON DELETE CASCADE;

View File

@@ -1,3 +1,3 @@
ALTER TABLE sp_distribution_set ADD COLUMN valid BOOLEAN;
UPDATE sp_distribution_set SET valid = 1;
UPDATE sp_distribution_set SET valid = TRUE;

View File

@@ -1,3 +1,3 @@
ALTER TABLE sp_base_software_module ADD COLUMN encrypted BOOLEAN;
UPDATE sp_base_software_module SET encrypted = 0;
UPDATE sp_base_software_module SET encrypted = FALSE;

View File

@@ -1 +1 @@
ALTER TABLE sp_target_type ALTER COLUMN name VARCHAR (128);
ALTER TABLE sp_target_type ALTER COLUMN name TYPE VARCHAR (128);

View File

@@ -1,4 +1,4 @@
ALTER TABLE sp_action_status ADD code INTEGER;
ALTER TABLE sp_action_status ADD COLUMN code INTEGER;
CREATE INDEX sp_idx_action_status_03
ON sp_action_status
USING BTREE (tenant, code);

View File

@@ -0,0 +1,26 @@
ALTER TABLE sp_rolloutgroup
ADD COLUMN confirmation_required BOOLEAN;
ALTER TABLE sp_target_filter_query
ADD COLUMN confirmation_required BOOLEAN;
UPDATE sp_rolloutgroup SET confirmation_required = FALSE;
UPDATE sp_target_filter_query SET confirmation_required = FALSE;
CREATE TABLE sp_target_conf_status
(
id BIGSERIAL,
target_id BIGINT NOT NULL,
initiator VARCHAR(64),
remark VARCHAR(512),
tenant VARCHAR(40) NOT NULL,
created_at BIGINT NOT NULL,
created_by VARCHAR(64) NOT NULL,
last_modified_at BIGINT NOT NULL,
last_modified_by VARCHAR(64) NOT NULL,
optlock_revision BIGINT NULL
);
ALTER TABLE sp_target_conf_status
ADD CONSTRAINT pk_sp_target_conf_status PRIMARY KEY (id);

View File

@@ -0,0 +1,19 @@
ALTER TABLE sp_rolloutgroup ADD confirmation_required BIT DEFAULT 0;
ALTER TABLE sp_target_filter_query ADD confirmation_required BIT DEFAULT 0;
CREATE TABLE sp_target_conf_status
(
id NUMERIC(19) IDENTITY NOT NULL,
target_id NUMERIC(19) NOT NULL,
initiator VARCHAR(64),
remark VARCHAR(512),
tenant VARCHAR(40) NOT NULL,
created_at NUMERIC(19) NOT NULL,
created_by VARCHAR(64) NOT NULL,
last_modified_at NUMERIC(19) NOT NULL,
last_modified_by VARCHAR(64) NOT NULL,
optlock_revision INTEGER NULL,
PRIMARY KEY (id)
);
ALTER TABLE sp_target_conf_status
ADD CONSTRAINT fk_target_auto_conf FOREIGN KEY (target_id) REFERENCES sp_target (id) ON DELETE CASCADE;

View File

@@ -44,8 +44,8 @@ public class RolloutEventTest extends AbstractRemoteEntityEventTest<Rollout> {
.type("os").modules(Collections.singletonList(module.getId())));
return rolloutManagement.create(
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds),
5, new RolloutGroupConditionBuilder().withDefaults()
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds), 5,
false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build());
}

View File

@@ -84,8 +84,8 @@ public class RolloutGroupEventTest extends AbstractRemoteEntityEventTest<Rollout
.type("os").modules(Collections.singletonList(module.getId())));
final Rollout entity = rolloutManagement.create(
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds),
5, new RolloutGroupConditionBuilder().withDefaults()
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds), 5,
false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build());
return rolloutGroupManagement.findByRollout(PAGE, entity.getId()).getContent().get(0);

View File

@@ -117,7 +117,7 @@ public class ConcurrentDistributionSetInvalidationTest extends AbstractJpaIntegr
return rolloutManagement.create(entityFactory.rollout().create()
.name("verifyInvalidateDistributionSetWithLargeRolloutThrowsException").description("desc")
.targetFilterQuery("name==*").set(distributionSet).actionType(ActionType.FORCED),
quotaManagement.getMaxRolloutGroupsPerRollout(), conditions);
quotaManagement.getMaxRolloutGroupsPerRollout(), false, conditions);
}
}

View File

@@ -0,0 +1,315 @@
/**
* Copyright (c) 2022 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.jpa;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.hawkbit.repository.exception.AutoConfirmationAlreadyActiveException;
import org.eclipse.hawkbit.repository.exception.InvalidConfirmationFeedbackException;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Target;
import org.junit.jupiter.api.Test;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
/**
* Test class testing the functionality of triggering a deployment of
* {@link DistributionSet}s to {@link Target}s with AutoConfirmation active.
*
*/
@Feature("Component Tests - Repository")
@Story("Confirmation Management")
class ConfirmationManagementTest extends AbstractJpaIntegrationTest {
@Test
@Description("Verify 'findActiveActionsWaitingConfirmation' method is filtering like expected")
void retrieveActionsWithConfirmationState() {
enableConfirmationFlow();
final String controllerId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
final List<Action> actions = assignDistributionSet(dsId, controllerId).getAssignedEntity();
assertThat(actions).hasSize(1);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
final Long dsId2 = testdataFactory.createDistributionSet().getId();
// ds1 will be in canceling state afterwards
assignDistributionSet(dsId2, controllerId);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
}
@Test
@Description("Verify 'findActiveActionsWaitingConfirmation' method is filtering like expected with multi assignment active")
void retrieveActionsWithConfirmationStateInMultiAssignment() {
enableMultiAssignments();
enableConfirmationFlow();
final String controllerId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
final List<Action> actions = assignDistributionSet(dsId, controllerId).getAssignedEntity();
assertThat(actions).hasSize(1);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
final Long dsId2 = testdataFactory.createDistributionSet().getId();
assignDistributionSet(dsId2, controllerId);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(2)
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
confirmationManagement.confirmAction(actions.get(0).getId(), null, null);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION
&& Objects.equals(action.getDistributionSet().getId(), dsId2));
}
@Test
@Description("Verify confirming an action will put it to the running state")
void confirmedActionWillSwitchToRunningState() {
enableConfirmationFlow();
final String controllerId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
final List<Action> actions = assignDistributionSet(dsId, controllerId).getAssignedEntity();
assertThat(actions).hasSize(1).allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
assertThat(controllerManagement.findActionStatusByAction(PAGE, actions.get(0).getId())).hasSize(1)
.allMatch(status -> status.getStatus() == Status.WAIT_FOR_CONFIRMATION);
final Action newAction = confirmationManagement.confirmAction(actions.get(0).getId(), null, null);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).isEmpty();
// verify action in RUNNING state
assertThat(newAction.getStatus()).isEqualTo(Status.RUNNING);
// status entry RUNNING should be present in status history
assertThat(controllerManagement.findActionStatusByAction(PAGE, newAction.getId())).hasSize(2)
.anyMatch(status -> status.getStatus() == Status.RUNNING);
}
@Test
@Description("Verify confirming an confirmed action will lead to a specific failure")
void confirmedActionCannotBeConfirmedAgain() {
enableConfirmationFlow();
final String controllerId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
final List<Action> actions = assignDistributionSet(dsId, controllerId).getAssignedEntity();
assertThat(actions).hasSize(1).allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
final Action newAction = confirmationManagement.confirmAction(actions.get(0).getId(), null, null);
// verify action in RUNNING state
assertThat(newAction.getStatus()).isEqualTo(Status.RUNNING);
assertThatThrownBy(() -> confirmationManagement.confirmAction(actions.get(0).getId(), null, null))
.isInstanceOf(InvalidConfirmationFeedbackException.class)
.matches(e -> ((InvalidConfirmationFeedbackException) e)
.getReason() == InvalidConfirmationFeedbackException.Reason.NOT_AWAITING_CONFIRMATION);
}
@Test
@Description("Verify confirming a closed action will lead to a specific failure")
void confirmedActionCannotBeGivenOnFinishedAction() {
enableConfirmationFlow();
final Action action = prepareFinishedUpdate();
assertThatThrownBy(() -> confirmationManagement.confirmAction(action.getId(), null, null))
.isInstanceOf(InvalidConfirmationFeedbackException.class)
.matches(e -> ((InvalidConfirmationFeedbackException) e)
.getReason() == InvalidConfirmationFeedbackException.Reason.ACTION_CLOSED);
}
@Test
@Description("Verify denying an action will leave it in WFC state")
void deniedActionWillStayInWfcState() {
enableConfirmationFlow();
final String controllerId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
final List<Action> actions = assignDistributionSet(dsId, controllerId).getAssignedEntity();
assertThat(actions).hasSize(1).allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
assertThat(controllerManagement.findActionStatusByAction(PAGE, actions.get(0).getId())).hasSize(1)
.allMatch(status -> status.getStatus() == Status.WAIT_FOR_CONFIRMATION);
final Action newAction = confirmationManagement.denyAction(actions.get(0).getId(), null, null);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
// verify action still in WFC state
assertThat(newAction.getStatus()).isEqualTo(Status.WAIT_FOR_CONFIRMATION);
// no status entry RUNNING should be present in status history
assertThat(controllerManagement.findActionStatusByAction(PAGE, newAction.getId())).hasSize(2)
.noneMatch(status -> status.getStatus() == Status.RUNNING);
}
@Test
@Description("Verify multiple actions in WFC state will be transferred in RUNNING state in case auto-confirmation is activated.")
void activateAutoConfirmationInMultiAssignment() {
enableMultiAssignments();
enableConfirmationFlow();
final String controllerId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
final Long dsId2 = testdataFactory.createDistributionSet().getId();
final List<Action> actions = assignDistributionSets(
Arrays.asList(toDeploymentRequest(controllerId, dsId), toDeploymentRequest(controllerId, dsId2)))
.stream().flatMap(s -> s.getAssignedEntity().stream()).collect(Collectors.toList());
assertThat(actions).hasSize(2);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(2)
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
confirmationManagement.activateAutoConfirmation(controllerId, null, null);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).isEmpty();
assertThat(deploymentManagement.findActionsByTarget(controllerId, PAGE).getContent()).hasSize(2)
.allMatch(action -> action.getStatus() == Status.RUNNING);
}
@Test
@Description("Verify action in WFC state will be transferred in RUNNING state in case auto-confirmation is activated.")
void activateAutoConfirmationOnActiveAction() {
enableConfirmationFlow();
final String controllerId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
// do assignment and verify
assertThat(assignDistributionSet(dsId, controllerId).getAssignedEntity()).hasSize(1);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
confirmationManagement.activateAutoConfirmation(controllerId, null, null);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).isEmpty();
assertThat(deploymentManagement.findActionsByTarget(controllerId, PAGE).getContent()).hasSize(1)
.allMatch(action -> action.getStatus() == Status.RUNNING);
}
@Test
@Description("Verify created action after activating auto confirmation is directly in running state.")
void activateAutoConfirmationAndCreateAction() {
enableConfirmationFlow();
final String controllerId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).isEmpty();
confirmationManagement.activateAutoConfirmation(controllerId, null, null);
// do assignment and verify
assertThat(assignDistributionSet(dsId, controllerId).getAssignedEntity()).hasSize(1);
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).isEmpty();
assertThat(deploymentManagement.findActionsByTarget(controllerId, PAGE).getContent()).hasSize(1)
.allMatch(action -> action.getStatus() == Status.RUNNING);
}
@ParameterizedTest
@MethodSource("getAutoConfirmationArguments")
@Description("Verify activating auto confirmation with different parameters")
void verifyAutoConfirmationActivationValues(final String initiator, final String remark) {
final String controllerId = testdataFactory.createTarget().getControllerId();
confirmationManagement.activateAutoConfirmation(controllerId, initiator, remark);
assertThat(targetManagement.getByControllerID(controllerId)).hasValueSatisfying(target -> {
assertThat(target.getAutoConfirmationStatus()).isNotNull()
.matches(status -> status.getTarget().getControllerId().equals(controllerId))
.matches(status -> Objects.equals(status.getInitiator(), initiator))
.matches(status -> Objects.equals(status.getCreatedBy(), "bumlux"))
.matches(status -> Objects.equals(status.getRemark(), remark)).satisfies(status -> {
final Instant activationTime = Instant.ofEpochMilli(status.getActivatedAt());
assertThat(activationTime).isAfterOrEqualTo(activationTime.minusSeconds(3L));
});
});
confirmationManagement.deactivateAutoConfirmation(controllerId);
verifyAutoConfirmationIsDisabled(controllerId);
}
private static Stream<Arguments> getAutoConfirmationArguments() {
return Stream.of(Arguments.of("TestUser", "TestRemark"), Arguments.of("TestUser", null),
Arguments.of(null, "TestRemark"), Arguments.of(null, null));
}
@Test
@Description("Verify activating already active auto confirmation will throw exception.")
void verifyActivateAlreadyActiveAutoConfirmationThrowException() {
final String controllerId = testdataFactory.createTarget().getControllerId();
confirmationManagement.activateAutoConfirmation(controllerId, "any", "any");
assertThat(targetManagement.getByControllerID(controllerId))
.hasValueSatisfying(target -> assertThat(target.getAutoConfirmationStatus()).isNotNull());
assertThatThrownBy(() -> confirmationManagement.activateAutoConfirmation(controllerId, "any", "any"))
.isInstanceOf(AutoConfirmationAlreadyActiveException.class)
.hasMessage("Auto confirmation is already active for device " + controllerId);
}
@Test
@Description("Verify disabling already disabled auto confirmation will not have any affect.")
void disableAlreadyDisabledAutoConfirmationHaveNoAffect() {
final String controllerId = testdataFactory.createTarget().getControllerId();
verifyAutoConfirmationIsDisabled(controllerId);
confirmationManagement.deactivateAutoConfirmation(controllerId);
verifyAutoConfirmationIsDisabled(controllerId);
}
private void verifyAutoConfirmationIsDisabled(final String controllerId) {
assertThat(targetManagement.getByControllerID(controllerId))
.hasValueSatisfying(target -> assertThat(target.getAutoConfirmationStatus()).isNull());
}
private static DeploymentRequest toDeploymentRequest(final String controllerId, final Long distributionSetId) {
return new DeploymentRequestBuilder(controllerId, distributionSetId).setConfirmationRequired(true).build();
}
}

View File

@@ -796,6 +796,8 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getUpdateStatus())
.isEqualTo(TargetUpdateStatus.IN_SYNC);
assertThat(actionRepository.findById(action.getId()))
.hasValueSatisfying(a -> assertThat(a.getStatus()).isEqualTo(Status.FINISHED));
assertThat(actionStatusRepository.count()).isEqualTo(3);
assertThat(controllerManagement.findActionStatusByAction(PAGE, action.getId()).getNumberOfElements())
.isEqualTo(3);
@@ -806,7 +808,8 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
+ "configured to accept them.")
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1),
@Expect(type = ActionCreatedEvent.class, count = 1),
@Expect(type = ActionUpdatedEvent.class, count = 1),
@Expect(type = TargetUpdatedEvent.class, count = 2),
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
@Expect(type = TargetAttributesRequestedEvent.class, count = 1),

View File

@@ -10,6 +10,8 @@ package org.eclipse.hawkbit.repository.jpa;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.eclipse.hawkbit.repository.model.Action.Status.RUNNING;
import static org.eclipse.hawkbit.repository.model.Action.Status.WAIT_FOR_CONFIRMATION;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.AbstractMap.SimpleEntry;
@@ -19,8 +21,10 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.validation.ConstraintViolationException;
@@ -58,6 +62,7 @@ import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
import org.eclipse.hawkbit.repository.model.DistributionSetTag;
@@ -70,6 +75,8 @@ import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -284,7 +291,8 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
assignDistributionSet(cancelDs, targets).getAssignedEntity();
assertThat(deploymentManagement.countActionsAll()).isEqualTo(quotaManagement.getMaxTargetsPerAutoAssignment());
assignDistributionSet(cancelDs2, targets).getAssignedEntity();
assertThat(deploymentManagement.countActionsAll()).isEqualTo(2L * quotaManagement.getMaxTargetsPerAutoAssignment());
assertThat(deploymentManagement.countActionsAll())
.isEqualTo(2L * quotaManagement.getMaxTargetsPerAutoAssignment());
}
@Test
@@ -546,13 +554,13 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
final DistributionSet ds1 = testdataFactory.createDistributionSet("1");
assignDistributionSet(ds1, targets);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, RUNNING);
// Second assignment
final DistributionSet ds2 = testdataFactory.createDistributionSet("2");
assignDistributionSet(ds2, targets);
assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, Status.RUNNING);
assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, RUNNING);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_INACTIVE, Status.CANCELED);
assertThat(targetManagement.findByAssignedDistributionSet(PAGE, ds2.getId()).getContent()).hasSize(10)
@@ -583,14 +591,14 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
final DistributionSet ds1 = testdataFactory.createDistributionSet("Multi-assign-1");
assignDistributionSet(ds1.getId(), targetIds, 77);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, RUNNING);
// Second assignment
final DistributionSet ds2 = testdataFactory.createDistributionSet("Multi-assign-2");
assignDistributionSet(ds2.getId(), targetIds, 45);
assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, Status.RUNNING);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING);
assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, RUNNING);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, RUNNING);
}
private void assertDsExclusivelyAssignedToTargets(final List<Target> targets, final long dsId, final boolean active,
@@ -651,7 +659,8 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
void cancelMultiAssignmentActions() {
final List<Target> targets = testdataFactory.createTargets(1);
final List<DistributionSet> distributionSets = testdataFactory.createDistributionSets(4);
final List<DeploymentRequest> deploymentRequests = createAssignmentRequests(distributionSets, targets, 34);
final List<DeploymentRequest> deploymentRequests = createAssignmentRequests(distributionSets, targets, 34,
false);
enableMultiAssignments();
final List<DistributionSetAssignmentResult> results = deploymentManagement
@@ -672,9 +681,15 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
protected List<DeploymentRequest> createAssignmentRequests(final Collection<DistributionSet> distributionSets,
final Collection<Target> targets, final int weight) {
return createAssignmentRequests(distributionSets, targets, weight, false);
}
protected List<DeploymentRequest> createAssignmentRequests(final Collection<DistributionSet> distributionSets,
final Collection<Target> targets, final int weight, final boolean confirmationRequired) {
final List<DeploymentRequest> deploymentRequests = new ArrayList<>();
distributionSets.forEach(ds -> targets.forEach(target -> deploymentRequests.add(DeploymentManagement
.deploymentRequest(target.getControllerId(), ds.getId()).setWeight(weight).build())));
distributionSets.forEach(ds -> targets.forEach(target -> deploymentRequests
.add(DeploymentManagement.deploymentRequest(target.getControllerId(), ds.getId()).setWeight(weight)
.setConfirmationRequired(confirmationRequired).build())));
return deploymentRequests;
}
@@ -713,7 +728,7 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
knownTargetIds.add(notExistingId);
final List<DistributionSetAssignmentResult> assignDistributionSetsResults = assignDistributionSetToTargets(
createdDs, knownTargetIds);
createdDs, knownTargetIds, false);
for (final DistributionSetAssignmentResult assignDistributionSetsResult : assignDistributionSetsResults) {
assertThat(assignDistributionSetsResult.getAlreadyAssigned()).isZero();
@@ -722,12 +737,152 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
}
}
@ParameterizedTest
@ValueSource(booleans = { true, false })
@Description("Assignments with confirmation flow active will result in actions in 'WAIT_FOR_CONFIRMATION' state")
void assignmentWithConfirmationFlowActive(final boolean confirmationRequired) {
final List<String> controllerIds = testdataFactory.createTargets(1).stream().map(Target::getControllerId)
.collect(Collectors.toList());
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
enableConfirmationFlow();
List<DistributionSetAssignmentResult> results = assignDistributionSetToTargets(distributionSet, controllerIds,
confirmationRequired);
assertThat(getResultingActionCount(results)).isEqualTo(controllerIds.size());
controllerIds.forEach(controllerId -> {
actionRepository.findByTargetControllerId(PAGE, controllerId).forEach(action -> {
assertThat(action.getDistributionSet().getId()).isIn(distributionSet.getId());
assertThat(action.getInitiatedBy()).as("Should be Initiated by current user")
.isEqualTo(tenantAware.getCurrentUsername());
if (confirmationRequired) {
assertThat(action.getStatus()).isEqualTo(Status.WAIT_FOR_CONFIRMATION);
} else {
assertThat(action.getStatus()).isEqualTo(RUNNING);
}
});
});
}
@ParameterizedTest
@ValueSource(booleans = { true, false })
@Description("Verify auto confirmation assignments and check action status with messages")
void assignmentWithAutoConfirmationWillBeHandledCorrectly(final boolean confirmationRequired) {
enableConfirmationFlow();
final Target target = testdataFactory.createTarget();
assertThat(target.getAutoConfirmationStatus()).isNull();
confirmationManagement.activateAutoConfirmation(target.getControllerId(), "not_bumlux", "my personal remark");
assertThat(targetManagement.getByControllerID(target.getControllerId()))
.hasValueSatisfying(t -> assertThat(t.getAutoConfirmationStatus()).isNotNull());
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
assignDistributionSets(Collections
.singletonList(new DeploymentRequestBuilder(target.getControllerId(), distributionSet.getId())
.setConfirmationRequired(confirmationRequired).build()));
assertThat(deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent()).hasSize(1)
.allSatisfy(action -> {
assertThat(action.getStatus()).isEqualTo(RUNNING);
assertThat(actionStatusRepository.getByActionId(PAGE, action.getId())).hasSize(1)
.allSatisfy(status -> {
final JpaActionStatus actionStatus = (JpaActionStatus) status;
assertThat(actionStatus.getStatus()).isEqualTo(WAIT_FOR_CONFIRMATION);
if (confirmationRequired) {
// confirmation of assignment is basically required, but active
// auto-confirmation will perform the confirmation
assertThat(actionStatus.getMessages())
.contains("Assignment initiated by user 'bumlux'")
.contains("Assignment automatically confirmed by initiator 'not_bumlux'. \n"
+ "\n" + "Auto confirmation activated by system user: 'bumlux' \n"
+ "\n" + "Remark: my personal remark");
} else {
// assignment never required confirmation, auto-confirmation will not be
// applied.
// assignment initiator has confirmed the action already
assertThat(actionStatus.getMessages())
.contains("Assignment initiated by user 'bumlux'")
.contains("Assignment confirmed by initiator [bumlux].");
}
});
});
}
@Test
@Description("Multiple assignments with confirmation flow active will result in correct cancel behaviour")
void multipleAssignmentWithConfirmationFlowActiveVerifyCancelBehaviour() {
final Target target = testdataFactory.createTarget("firstDevice");
final DistributionSet firstDs = testdataFactory.createDistributionSet();
final DistributionSet secondDs = testdataFactory.createDistributionSet();
enableConfirmationFlow();
final List<Action> resultActions = assignDistributionSet(firstDs.getId(), target.getControllerId())
.getAssignedEntity();
assertThat(resultActions).hasSize(1);
assertThat(resultActions.get(0)).satisfies(action -> {
assertThat(action.getDistributionSet().getId()).isEqualTo(firstDs.getId());
assertThat(action.getStatus()).isEqualTo(Status.WAIT_FOR_CONFIRMATION);
});
final List<Action> resultActions2 = assignDistributionSet(secondDs.getId(), target.getControllerId())
.getAssignedEntity();
assertThat(resultActions2).hasSize(1);
assertThat(resultActions2.get(0)).satisfies(action -> {
assertThat(action.getDistributionSet().getId()).isEqualTo(secondDs.getId());
assertThat(action.getStatus()).isEqualTo(Status.WAIT_FOR_CONFIRMATION);
});
final List<Action> actions = actionRepository.findByTargetControllerId(PAGE, target.getControllerId())
.getContent();
assertThat(actions).hasSize(2)
.anyMatch(action -> Objects.equals(action.getDistributionSet().getId(), firstDs.getId())
&& action.getStatus() == Status.CANCELING)
.anyMatch(action -> Objects.equals(action.getDistributionSet().getId(), secondDs.getId())
&& action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
}
@Test
@Description("Assignments with confirmation flow deactivated will result in actions in only in 'RUNNING' state")
void verifyConfirmationRequiredFlagHaveNoInfluenceIfFlowIsDeactivated() {
final List<String> targets1 = testdataFactory.createTargets("group1", 1).stream().map(Target::getControllerId)
.collect(Collectors.toList());
final List<String> targets2 = testdataFactory.createTargets("group2", 1).stream().map(Target::getControllerId)
.collect(Collectors.toList());
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
final List<DistributionSetAssignmentResult> results = Stream
.concat(assignDistributionSetToTargets(distributionSet, targets1, true).stream(), //
assignDistributionSetToTargets(distributionSet, targets2, false).stream()) //
.collect(Collectors.toList());
final List<String> controllerIds = Stream.concat(targets1.stream(), targets2.stream())
.collect(Collectors.toList());
assertThat(getResultingActionCount(results)).isEqualTo(controllerIds.size());
controllerIds.forEach(controllerId -> {
actionRepository.findByTargetControllerId(PAGE, controllerId).forEach(action -> {
assertThat(action.getDistributionSet().getId()).isIn(distributionSet.getId());
assertThat(action.getInitiatedBy()).as("Should be Initiated by current user")
.isEqualTo(tenantAware.getCurrentUsername());
assertThat(action.getStatus()).isEqualTo(RUNNING);
});
});
}
private List<DistributionSetAssignmentResult> assignDistributionSetToTargets(final DistributionSet distributionSet,
final Iterable<String> targetIds) {
final Iterable<String> targetIds, final boolean confirmationRequired) {
final List<DeploymentRequest> deploymentRequests = new ArrayList<>();
for (final String controllerId : targetIds) {
deploymentRequests.add(new DeploymentRequest(controllerId, distributionSet.getId(), ActionType.FORCED, 0,
null, null, null, null));
null, null, null, null, confirmationRequired));
}
return deploymentManagement.assignDistributionSets(deploymentRequests);
}

View File

@@ -22,6 +22,7 @@ import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
@@ -31,6 +32,7 @@ import org.assertj.core.api.Assertions;
import org.assertj.core.api.Condition;
import org.awaitility.Awaitility;
import org.awaitility.Duration;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
@@ -81,6 +83,9 @@ import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -139,6 +144,42 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
assertThat(rolloutCreatedAction.getStatus()).isEqualTo(Status.FINISHED);
}
@ParameterizedTest
@MethodSource("simpleRolloutsPossibilities")
@Description("Verifies that action states are correctly initialized after starting a rollout with different options in regard to the confirmation.")
void runRolloutWithConfirmationFlagAndCoonfirmationFlowOptions(final boolean confirmationFlowActive,
final boolean confirmationRequired, final Status expectedStatus) {
// manually assign distribution set to target
final String knownControllerId = "controller12345";
final DistributionSet knownDistributionSet = testdataFactory.createDistributionSet();
testdataFactory.createTarget(knownControllerId);
if (confirmationFlowActive) {
enableConfirmationFlow();
}
// create rollout with the same distribution set already assigned
// start rollout
final Rollout rollout = testdataFactory.createRolloutByVariables("rolloutNotCancelRunningAction", "description",
1, "name==*", knownDistributionSet, "50", "5", confirmationRequired);
rolloutManagement.start(rollout.getId());
rolloutManagement.handleRollouts();
// verify that manually created action is still running and action
// created from rollout is finished
final List<Action> actionsByKnownTarget = deploymentManagement.findActionsByTarget(knownControllerId, PAGE)
.getContent();
assertThat(actionsByKnownTarget).hasSize(1);
assertThat(actionsByKnownTarget.get(0).getStatus()).isEqualTo(expectedStatus);
}
private static Stream<Arguments> simpleRolloutsPossibilities() {
return Stream.of(Arguments.of(true, true, Status.WAIT_FOR_CONFIRMATION), //
Arguments.of(true, false, Status.RUNNING), //
Arguments.of(false, true, Status.RUNNING), //
Arguments.of(false, false, Status.RUNNING));//
}
@Test
@Description("Verifies that a running action is auto canceled by a rollout which assigns another distribution-set.")
void rolloutAssignsNewDistributionSetAndAutoCloseActiveActions() {
@@ -1369,7 +1410,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
.targetFilterQuery("controllerId==" + targetPrefixName + "-*").set(distributionSet);
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> rolloutManagement.create(rollout, amountGroups, conditions));
.isThrownBy(() -> rolloutManagement.create(rollout, amountGroups, false, conditions));
}
@Test
@@ -1540,6 +1581,117 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
}
@Test
@Description("Verify rollout execution with advanced group definition and confirmation flow active.")
void createRolloutWithGroupDefinitionAndConfirmationFlowActive() {
final String rolloutName = "rolloutTest4";
final int amountTargetsInGroup1 = 10;
final int amountTargetsInGroup2 = 20;
final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults().build();
// Generate Targets for group 2
final RolloutCreate rolloutcreate = generateTargetsAndRollout(rolloutName, amountTargetsInGroup2);
// Generate Targets for group 1
testdataFactory.createTargets(amountTargetsInGroup1, rolloutName + "-gr1-", rolloutName);
final List<RolloutGroupCreate> rolloutGroups = new ArrayList<>(3);
rolloutGroups.add(generateRolloutGroup(0, 100, "id==" + rolloutName + "-gr1-*", true));
rolloutGroups.add(generateRolloutGroup(1, 100, null, false));
// enable confirmation flow
enableConfirmationFlow();
final Long rolloutId = rolloutManagement.create(rolloutcreate, rolloutGroups, conditions).getId();
assertThat(getRollout(rolloutId)).satisfies(rollout -> {
assertThat(rollout.getStatus()).isEqualTo(RolloutStatus.CREATING);
for (final RolloutGroup group : rolloutGroupManagement.findByRollout(PAGE, rollout.getId()).getContent()) {
assertThat(group.getStatus()).isEqualTo(RolloutGroupStatus.CREATING);
}
});
// first handle iteration will put rollout in ready state
rolloutManagement.handleRollouts();
assertThat(getRollout(rolloutId)).satisfies(rollout -> {
assertThat(rollout.getStatus()).isEqualTo(RolloutStatus.READY);
assertThat(rollout.getTotalTargets()).isEqualTo(amountTargetsInGroup1 + amountTargetsInGroup2);
});
// verify created rollout groups
final List<Long> rolloutGroupIds = rolloutGroupManagement.findByRollout(PAGE, rolloutId).getContent().stream()
.map(Identifiable::getId).collect(Collectors.toList());
assertThat(rolloutGroupIds).hasSize(2);
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.READY, true, amountTargetsInGroup1, null);
assertRolloutGroup(rolloutGroupIds.get(1), RolloutGroupStatus.READY, false, amountTargetsInGroup2, null);
// start rollout
rolloutManagement.start(rolloutId);
rolloutManagement.handleRollouts();
// verify rollout started. Check groups are in right state.
// Group 1 should be in WFC state, since confirmation is required here.
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.RUNNING, true, amountTargetsInGroup1,
Status.WAIT_FOR_CONFIRMATION);
assertRolloutGroup(rolloutGroupIds.get(1), RolloutGroupStatus.SCHEDULED, false, amountTargetsInGroup2,
Status.SCHEDULED);
// cancel execution of all action of group 1 to trigger second group
forceQuitAllActionsOfRolloutGroup(rolloutGroupIds.get(0));
rolloutManagement.handleRollouts();
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.FINISHED, true, amountTargetsInGroup1,
Status.CANCELED);
assertRolloutGroup(rolloutGroupIds.get(1), RolloutGroupStatus.SCHEDULED, false, amountTargetsInGroup2,
Status.SCHEDULED);
// verify actions of second rule are directly in RUNNING state, since
// confirmation is not required for this group
rolloutManagement.handleRollouts();
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.FINISHED, true, amountTargetsInGroup1,
Status.CANCELED);
assertRolloutGroup(rolloutGroupIds.get(1), RolloutGroupStatus.RUNNING, false, amountTargetsInGroup2,
Status.RUNNING);
}
private void assertRolloutGroup(final long rolloutGroupId, final RolloutGroupStatus status,
final boolean isConfirmationRequired, final long totalTargets, final Status actionStatusToCheck) {
assertThat(rolloutGroupManagement.get(rolloutGroupId)).hasValueSatisfying(rolloutGroup -> {
assertThat(rolloutGroup.getStatus()).isEqualTo(status);
assertThat(rolloutGroup.isConfirmationRequired()).isEqualTo(isConfirmationRequired);
assertThat(rolloutGroup.getTotalTargets()).isEqualTo(totalTargets);
if (actionStatusToCheck != null) {
assertAllActionOfRolloutGroupHavingStatus(rolloutGroup.getId(), actionStatusToCheck);
}
});
}
private void assertAllActionOfRolloutGroupHavingStatus(final long rolloutGroupId, final Status status) {
final List<Target> targets = rolloutGroupManagement.findTargetsOfRolloutGroup(PAGE, rolloutGroupId)
.getContent();
targets.forEach(target -> {
final List<Action> activeActions = deploymentManagement
.findActionsByTarget(target.getControllerId(), PAGE).getContent();
assertThat(activeActions).hasSize(1);
assertThat(activeActions.get(0).getStatus()).isEqualTo(status);
});
}
private void forceQuitAllActionsOfRolloutGroup(final long rolloutGroupId) {
final List<Target> targets = rolloutGroupManagement.findTargetsOfRolloutGroup(PAGE, rolloutGroupId)
.getContent();
targets.forEach(target -> {
deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId()).getContent().stream().map(Identifiable::getId)
.forEach(actionId -> {
deploymentManagement.cancelAction(actionId);
deploymentManagement.forceQuitAction(actionId);
});
});
}
@Test
@Description("Verify rollout creation fails if group definition does not address all targets")
void createRolloutWithGroupsNotMatchingTargets() {
@@ -1593,7 +1745,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
final RolloutCreate rollout = generateTargetsAndRollout(rolloutName, targets);
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> rolloutManagement.create(rollout, maxGroups + 1, conditions))
.isThrownBy(() -> rolloutManagement.create(rollout, maxGroups + 1, false, conditions))
.withMessageContaining("not be greater than " + maxGroups);
}
@@ -1615,7 +1767,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
final RolloutCreate rolloutToCreate = entityFactory.rollout().create().name(rolloutName)
.description("some description").targetFilterQuery("id==" + rolloutName + "-*").set(distributionSet);
Rollout myRollout = rolloutManagement.create(rolloutToCreate, amountGroups, conditions);
Rollout myRollout = rolloutManagement.create(rolloutToCreate, amountGroups, false, conditions);
myRollout = getRollout(myRollout.getId());
assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.CREATING);
@@ -1787,7 +1939,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
.name(prefixRolloutRunning + "-testRollout").targetFilterQuery("name==" + randomString + "*")
.set(testDs);
Rollout rolloutRunning = rolloutManagement.create(rolloutRunningCreate, 1, conditions);
Rollout rolloutRunning = rolloutManagement.create(rolloutRunningCreate, 1, false, conditions);
// Let the executor handle created Rollout
rolloutManagement.handleRollouts();
// start the rollout, so it has active running actions and a group which
@@ -1799,7 +1951,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
final String prefixRolloutReady = randomString + "2";
final RolloutCreate rolloutReadyCreate = entityFactory.rollout().create()
.name(prefixRolloutReady + "-testRollout").targetFilterQuery("name==" + randomString + "*").set(testDs);
Rollout rolloutReady = rolloutManagement.create(rolloutReadyCreate, 1, conditions);
Rollout rolloutReady = rolloutManagement.create(rolloutReadyCreate, 1, false, conditions);
// Let the executor handle created Rollout
rolloutManagement.handleRollouts();
rolloutReady = reloadRollout(rolloutReady);
@@ -1959,7 +2111,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
final RolloutCreate rolloutToCreate = entityFactory.rollout().create().name(rolloutName)
.targetFilterQuery("name==*").set(testDs);
final Rollout createdRollout = rolloutManagement.create(rolloutToCreate, 1, conditions);
final Rollout createdRollout = rolloutManagement.create(rolloutToCreate, 1, false, conditions);
// Let the executor handle created Rollout
rolloutManagement.handleRollouts();
@@ -1982,8 +2134,14 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
private RolloutGroupCreate generateRolloutGroup(final int index, final Integer percentage,
final String targetFilter) {
return generateRolloutGroup(index, percentage, targetFilter, false);
}
private RolloutGroupCreate generateRolloutGroup(final int index, final Integer percentage,
final String targetFilter, final boolean confirmationRequired) {
return entityFactory.rolloutGroup().create().name("Group" + index).description("Group" + index + "desc")
.targetPercentage(Float.valueOf(percentage)).targetFilterQuery(targetFilter);
.targetPercentage(Float.valueOf(percentage)).targetFilterQuery(targetFilter)
.confirmationRequired(confirmationRequired);
}
private RolloutCreate generateTargetsAndRollout(final String rolloutName, final int amountTargetsForRollout) {
@@ -2033,7 +2191,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
testdataFactory.createTargets(amountTargetsForRollout, targetPrefixName + "-", targetPrefixName);
return testdataFactory.createRolloutByVariables(rolloutName, rolloutName + "description", groupSize,
"controllerId==" + targetPrefixName + "-*", dsForRolloutTwo, successCondition, errorCondition,
Action.ActionType.FORCED, weight);
Action.ActionType.FORCED, weight, false);
}
private int changeStatusForAllRunningActions(final Rollout rollout, final Status status) {

View File

@@ -33,6 +33,9 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.model.TargetType;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
@@ -174,6 +177,69 @@ class AutoAssignCheckerIntTest extends AbstractJpaIntegrationTest {
verifyThatTargetsNotHaveDistributionSetAssignment(toAssignDs, targets.subList(1, 25));
}
@ParameterizedTest
@MethodSource("confirmationOptions")
@Description("Test auto assignment of a DS to filtered targets with different confirmation options")
void checkAutoAssignWithConfirmationOptions(final boolean confirmationFlowActive, final boolean confirmationRequired,
final Action.Status expectedStatus) {
final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsA");
if (confirmationFlowActive) {
enableConfirmationFlow();
}
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.updateAutoAssignDS(entityFactory
.targetFilterQuery()
.updateAutoAssign(targetFilterQueryManagement
.create(entityFactory.targetFilterQuery().create().name("filterA").query("name==*")).getId())
.ds(distributionSet.getId()).confirmationRequired(confirmationRequired));
final String targetDsAIdPref = "targ";
final List<Target> targets = testdataFactory.createTargets(20, targetDsAIdPref,
targetDsAIdPref.concat(" description"));
// Run the check
autoAssignChecker.checkAllTargets();
verifyThatTargetsHaveDistributionSetAssignedAndActionStatus(distributionSet, targets, expectedStatus);
}
@ParameterizedTest
@MethodSource("confirmationOptions")
@Description("Test auto assignment of a DS for a specific device with different confirmation options")
void checkAutoAssignmentForDeviceWithConfirmationRequired(final boolean confirmationFlowActive,
final boolean confirmationRequired, final Action.Status expectedStatus) {
final DistributionSet toAssignDs = testdataFactory.createDistributionSet();
if (confirmationFlowActive) {
enableConfirmationFlow();
}
// target filter query that matches all targets
targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery()
.updateAutoAssign(targetFilterQueryManagement
.create(entityFactory.targetFilterQuery().create().name("filterA").query("name==*")).getId())
.ds(toAssignDs.getId()).confirmationRequired(confirmationRequired));
final List<Target> targets = testdataFactory.createTargets(25);
// Run the check
autoAssignChecker.checkSingleTarget(targets.get(0).getControllerId());
verifyThatTargetsHaveDistributionSetAssignedAndActionStatus(toAssignDs, targets.subList(0, 1), expectedStatus);
verifyThatTargetsNotHaveDistributionSetAssignment(toAssignDs, targets.subList(1, 25));
}
private static Stream<Arguments> confirmationOptions() {
return Stream.of( //
Arguments.of(true, true, Status.WAIT_FOR_CONFIRMATION), //
Arguments.of(true, false, Status.RUNNING), //
Arguments.of(false, true, Status.RUNNING), //
Arguments.of(false, false, Status.RUNNING));
}
@Test
@Description("Test auto assignment of an incomplete DS to filtered targets, that causes failures")
void checkAutoAssignWithFailures() {
@@ -246,6 +312,22 @@ class AutoAssignCheckerIntTest extends AbstractJpaIntegrationTest {
}
@Step
private void verifyThatTargetsHaveDistributionSetAssignedAndActionStatus(final DistributionSet set,
final List<Target> targets, final Action.Status status) {
final List<String> targetIds = targets.stream().map(Target::getControllerId).collect(Collectors.toList());
final List<Target> targetsWithAssignedDS = targetManagement.findByAssignedDistributionSet(PAGE, set.getId())
.getContent();
assertThat(targetsWithAssignedDS).isNotEmpty();
assertThat(targetsWithAssignedDS).allMatch(target -> targetIds.contains(target.getControllerId()));
final List<Action> actionsByDs = deploymentManagement.findActionsByDistributionSet(PAGE, set.getId())
.getContent();
assertThat(actionsByDs).hasSize(targets.size());
assertThat(actionsByDs).allMatch(action -> action.getStatus() == status);
}
@Step
private void verifyThatTargetsNotHaveDistributionSetAssignment(final DistributionSet set,
final List<Target> targets) {

View File

@@ -99,7 +99,7 @@ public class RSQLRolloutGroupFieldTest extends AbstractJpaIntegrationTest {
return rolloutManagement.create(
entityFactory.rollout().create().set(distributionSetManagement.get(distributionSetId).get()).name(name)
.targetFilterQuery(targetFilterQuery),
amountGroups, new RolloutGroupConditionBuilder().withDefaults()
amountGroups, false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
}
}

View File

@@ -28,6 +28,7 @@ import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
import org.eclipse.hawkbit.cache.TenantAwareCacheManager;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement;
@@ -158,6 +159,8 @@ public abstract class AbstractIntegrationTest {
@Autowired
protected DeploymentManagement deploymentManagement;
@Autowired
protected ConfirmationManagement confirmationManagement;
@Autowired
protected DistributionSetInvalidationManagement distributionSetInvalidationManagement;
@@ -235,9 +238,12 @@ public abstract class AbstractIntegrationTest {
protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final List<String> controllerIds,
final ActionType actionType, final long forcedTime, final Integer weight) {
final boolean confirmationFlowActive = isConfirmationFlowActive();
final List<DeploymentRequest> deploymentRequests = controllerIds.stream()
.map(id -> DeploymentManagement.deploymentRequest(id, dsID).setActionType(actionType)
.setForceTime(forcedTime).setWeight(weight).build())
.setForceTime(forcedTime).setWeight(weight).setConfirmationRequired(confirmationFlowActive)
.build())
.collect(Collectors.toList());
final List<DistributionSetAssignmentResult> results = deploymentManagement
.assignDistributionSets(deploymentRequests);
@@ -245,6 +251,13 @@ public abstract class AbstractIntegrationTest {
return results.get(0);
}
protected List<DistributionSetAssignmentResult> assignDistributionSets(final List<DeploymentRequest> requests) {
final List<DistributionSetAssignmentResult> distributionSetAssignmentResults = deploymentManagement
.assignDistributionSets(requests);
assertThat(distributionSetAssignmentResults).hasSize(requests.size());
return distributionSetAssignmentResults;
}
protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet ds,
final List<Target> targets) {
final List<String> targetIds = targets.stream().map(Target::getControllerId).collect(Collectors.toList());
@@ -296,7 +309,7 @@ public abstract class AbstractIntegrationTest {
return makeAssignment(DeploymentManagement.deploymentRequest(controllerId, dsID)
.setMaintenance(maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone)
.build());
.setConfirmationRequired(true).build());
}
protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet pset, final Target target) {
@@ -312,6 +325,19 @@ public abstract class AbstractIntegrationTest {
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, true);
}
protected void enableConfirmationFlow() {
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.USER_CONFIRMATION_ENABLED, true);
}
protected void disableConfirmationFlow() {
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.USER_CONFIRMATION_ENABLED, false);
}
protected boolean isConfirmationFlowActive() {
return tenantConfigurationManagement.getConfigurationValue(TenantConfigurationKey.USER_CONFIRMATION_ENABLED,
Boolean.class).getValue();
}
protected DistributionSetMetadata createDistributionSetMetadata(final long dsId, final MetaData md) {
return createDistributionSetMetadata(dsId, Collections.singletonList(md)).get(0);
}
@@ -345,6 +371,10 @@ public abstract class AbstractIntegrationTest {
Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
.getContent().get(0);
if (savedAction.getStatus() == Action.Status.WAIT_FOR_CONFIRMATION) {
confirmationManagement.confirmAction(savedAction.getId(), null, null);
}
savedAction = controllerManagement.addUpdateActionStatus(
entityFactory.actionStatus().create(savedAction.getId()).status(Action.Status.RUNNING));
@@ -473,4 +503,10 @@ public abstract class AbstractIntegrationTest {
protected void disableBatchAssignments() {
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.BATCH_ASSIGNMENTS_ENABLED, false);
}
protected boolean isConfirmationFlowEnabled() {
return tenantConfigurationManagement
.getConfigurationValue(TenantConfigurationKey.USER_CONFIRMATION_ENABLED, Boolean.class).getValue();
}
}

View File

@@ -877,10 +877,14 @@ public class TestdataFactory {
* @return {@link List} of {@link Target} entities
*/
public List<Target> createTargets(final int number) {
return createTargets(DEFAULT_CONTROLLER_ID, number);
}
public List<Target> createTargets(final String prefix, final int number) {
final List<TargetCreate> targets = Lists.newArrayListWithExpectedSize(number);
for (int i = 0; i < number; i++) {
targets.add(entityFactory.target().create().controllerId(DEFAULT_CONTROLLER_ID + i));
targets.add(entityFactory.target().create().controllerId(prefix + i));
}
return createTargets(targets);
@@ -1140,7 +1144,14 @@ public class TestdataFactory {
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
final String successCondition, final String errorCondition) {
return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet,
successCondition, errorCondition, Action.ActionType.FORCED, null);
successCondition, errorCondition, Action.ActionType.FORCED, null, false);
}
public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription,
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
final String successCondition, final String errorCondition, final boolean confirmationRequired) {
return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet,
successCondition, errorCondition, Action.ActionType.FORCED, null, confirmationRequired);
}
/**
@@ -1164,12 +1175,15 @@ public class TestdataFactory {
* the type of the Rollout
* @param weight
* weight of the Rollout
* @param confirmationRequired
* if the confirmation is required (considered with confirmation flow
* active)
* @return created {@link Rollout}
*/
public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription,
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
final String successCondition, final String errorCondition, final Action.ActionType actionType,
final Integer weight) {
final Integer weight, final boolean confirmationRequired) {
final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition)
.errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition)
@@ -1178,7 +1192,7 @@ public class TestdataFactory {
final Rollout rollout = rolloutManagement.create(
entityFactory.rollout().create().name(rolloutName).description(rolloutDescription)
.targetFilterQuery(filterQuery).set(distributionSet).actionType(actionType).weight(weight),
groupSize, conditions);
groupSize, confirmationRequired, conditions);
// Run here, because Scheduler is disabled during tests
rolloutManagement.handleRollouts();
@@ -1314,7 +1328,7 @@ public class TestdataFactory {
createTargets(amountOtherTargets, "others-" + suffix + "-", "rollout");
final String filterQuery = "controllerId==rollout-" + suffix + "-*";
return createRolloutByVariables("rollout-" + suffix, "test-rollout-description", amountOfGroups,
filterQuery, rolloutDS, successCondition, errorCondition, actionType, weight);
filterQuery, rolloutDS, successCondition, errorCondition, actionType, weight, false);
}
/**