Assign multiple distribution sets to a target via mgmt api (#886)

* Add multiassignment to mgmt api target endpoint
* Remove single assignment ds to targets offline
* Fix tests
* Add quota for maxResultingActionsPerManualAssignment
* Fix assignment with same target or distribution set multiple times in one request
* Log UI error
* Add tests
* Enable single assignment requests with multiple DSs and types
* Remove redundant target to DS assignment methods
* Add tests, fix assignment
* Fix possible nullpointer during target assignment request
* Update api docu
* Clean up deployment management code
* Enforce MaxActions quota for offline assignment
* Fix review findings
* Rename property, add migration into
* Add builder for DeploymentRequest
* Change offline assignment method to accept an assignment list, like online assignment
* Fix PR findings

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>
This commit is contained in:
Stefan Klotz
2019-09-17 14:20:26 +02:00
committed by Stefan Behl
parent dba972423b
commit 8687510131
50 changed files with 1466 additions and 559 deletions

View File

@@ -13,8 +13,12 @@
- ENTITYsrest classes have been removed; List<ENTITYrest> used instead (e.g. List<TargetRest> instead of TargetsRest)
### Renamed api annotations
- Annotation `org.eclipse.hawkbit.rest.resource.EnableRestResources` have changed to `org.eclipse.hawkbit.mgmt.annotation.EnableMgmtApi`
- Annotation `org.eclipse.hawkbit.ddi.resource.EnableDirectDeviceApi` have changed to `org.eclipse.hawkbit.ddi.annotation.EnableDdiApi`
- Annotation `org.eclipse.hawkbit.rest.resource.EnableRestResources` has changed to `org.eclipse.hawkbit.mgmt.annotation.EnableMgmtApi`
- Annotation `org.eclipse.hawkbit.ddi.resource.EnableDirectDeviceApi` has changed to `org.eclipse.hawkbit.ddi.annotation.EnableDdiApi`
### Renamed maven modules
- Module hawkbit-mgmt-api-client have changed to hawkbit-example-mgmt-simulator
- Module hawkbit-mgmt-api-client has changed to hawkbit-example-mgmt-simulator
## Milestone 0.3.0M6
### Configuration Property changes
- hawkbit.server.security.dos.maxTargetsPerManualAssignment has changed to hawkbit.server.security.dos.maxTargetDistributionSetAssignmentsPerManualAssignment

View File

@@ -240,7 +240,13 @@ public enum SpServerError {
* change is not allowed.
*/
SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED("hawkbit.server.error.repo.tenantConfigurationValueChangeNotAllowed",
"The requested tenant configuration value modification is not allowed.");
"The requested tenant configuration value modification is not allowed."),
/**
*
*/
SP_MULTIASSIGNMENT_NOT_ENABLED("hawkbit.server.error.multiassignmentNotEnabled",
"The requested operation requires Multiassignments to be enabled.");
private final String key;
private final String message;

View File

@@ -11,7 +11,6 @@ package org.eclipse.hawkbit.integration;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.hawkbit.amqp.AmqpProperties;
@@ -23,7 +22,6 @@ import org.eclipse.hawkbit.repository.model.Artifact;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.eclipse.hawkbit.security.DmfTenantSecurityToken;
import org.eclipse.hawkbit.security.DmfTenantSecurityToken.FileResource;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
@@ -193,8 +191,7 @@ public class AmqpAuthenticationMessageHandlerIntegrationTest extends AbstractAmq
final DmfTenantSecurityToken securityToken = createTenantSecurityToken(TENANT_EXIST, TARGET,
FileResource.createFileResourceBySha1(artifact.getSha1Hash()));
deploymentManagement.assignDistributionSet(distributionSet.getId(),
Arrays.asList(new TargetWithActionType(TARGET)));
assignDistributionSet(distributionSet.getId(), TARGET);
final Message returnMessage = sendAndReceiveAuthenticationMessage(securityToken);
verifyOkResult(returnMessage, artifact);
@@ -212,8 +209,7 @@ public class AmqpAuthenticationMessageHandlerIntegrationTest extends AbstractAmq
final DmfTenantSecurityToken securityToken = createTenantSecurityToken(TENANT_EXIST, target.getId(), null,
FileResource.createFileResourceBySha1(artifact.getSha1Hash()));
deploymentManagement.assignDistributionSet(distributionSet.getId(),
Arrays.asList(new TargetWithActionType(TARGET)));
assignDistributionSet(distributionSet.getId(), TARGET);
final Message returnMessage = sendAndReceiveAuthenticationMessage(securityToken);
verifyOkResult(returnMessage, artifact);
@@ -246,8 +242,7 @@ public class AmqpAuthenticationMessageHandlerIntegrationTest extends AbstractAmq
final DmfTenantSecurityToken securityToken = createTenantSecurityToken(TENANT_EXIST, TARGET,
FileResource.createFileResourceBySha1(artifact.getSha1Hash()));
deploymentManagement.assignDistributionSet(distributionSet.getId(),
Arrays.asList(new TargetWithActionType(TARGET)));
assignDistributionSet(distributionSet.getId(), TARGET);
final Message returnMessage = sendAndReceiveAuthenticationMessage(securityToken);
verifyOkResult(returnMessage, artifact);
@@ -309,8 +304,7 @@ public class AmqpAuthenticationMessageHandlerIntegrationTest extends AbstractAmq
final FileResource fileResource = FileResource.createFileResourceByArtifactId(artifact.getId());
final DmfTenantSecurityToken securityToken = createTenantSecurityToken(TENANT_EXIST, TARGET, fileResource);
deploymentManagement.assignDistributionSet(distributionSet.getId(),
Arrays.asList(new TargetWithActionType(TARGET)));
assignDistributionSet(distributionSet.getId(), TARGET);
final Message returnMessage = sendAndReceiveAuthenticationMessage(securityToken);
verifyOkResult(returnMessage, artifact);
@@ -365,8 +359,7 @@ public class AmqpAuthenticationMessageHandlerIntegrationTest extends AbstractAmq
softwareModule.getArtifact(artifact.getId()).get().getFilename());
final DmfTenantSecurityToken securityToken = createTenantSecurityToken(TENANT_EXIST, TARGET, fileResource);
deploymentManagement.assignDistributionSet(distributionSet.getId(),
Arrays.asList(new TargetWithActionType(TARGET)));
assignDistributionSet(distributionSet.getId(), TARGET);
final Message returnMessage = sendAndReceiveAuthenticationMessage(securityToken);
verifyOkResult(returnMessage, artifact);

View File

@@ -57,7 +57,6 @@ import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
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.Test;
import org.mockito.Mockito;
import org.springframework.amqp.core.Message;
@@ -464,10 +463,6 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer
createConditionFactory().until(() -> securityRule.runAsPrivileged(callable));
}
private void enableMultiAssignments() {
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, true);
}
private void assertLatestMultiActionMessageContainsInstallMessages(final String controllerId,
final List<Set<Long>> smIdsOfActionsExpected) {
final Message multiactionMessage = replyToListener.getLatestEventMessage(EventTopic.MULTI_ACTION);

View File

@@ -10,6 +10,7 @@ package org.eclipse.hawkbit.repository;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
@@ -21,20 +22,21 @@ import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEv
import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException;
import org.eclipse.hawkbit.repository.exception.QuotaExceededException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.model.Action;
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.DistributionSetType;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
@@ -48,71 +50,12 @@ import org.springframework.security.access.prepost.PreAuthorize;
public interface DeploymentManagement {
/**
* Assigns the addressed {@link DistributionSet} to all {@link Target}s by
* their IDs with a specific {@link ActionType} and {@code forcetime}.
* Assigns {@link DistributionSet}s to {@link Target}s according to the
* {@link DeploymentRequest}.
*
* @param dsID
* the ID of the distribution set to assign
* @param actionType
* the type of the action to apply on the assignment
* @param forcedTimestamp
* the time when the action should be forced, only necessary for
* {@link ActionType#TIMEFORCED}
* @param controllerIDs
* the IDs of the target to assign the distribution set
* @return the assignment result
*
* @throws IncompleteDistributionSetException
* if mandatory {@link SoftwareModuleType} are not assigned as
* define by the {@link DistributionSetType}.
*
* @throws EntityNotFoundException
* if either provided {@link DistributionSet} or {@link Target}s
* do not exist
* @param deploymentRequests
* information about all target-ds-assignments that shall be made
*
* @throws QuotaExceededException
* if the maximum number of targets the distribution set can be
* assigned to at once is exceeded
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
DistributionSetAssignmentResult assignDistributionSet(long dsID, @NotNull ActionType actionType,
long forcedTimestamp, @NotEmpty Collection<String> controllerIDs);
/**
* Assigns the addressed {@link DistributionSet} to all {@link Target}s by
* their IDs with a specific {@link ActionType} and {@code forcetime}.
*
* @param dsID
* the ID of the distribution set to assign
* @param targets
* a list of all targets and their action type
* @return the assignment result
*
* @throws IncompleteDistributionSetException
* if mandatory {@link SoftwareModuleType} are not assigned as
* define by the {@link DistributionSetType}.
*
* @throws EntityNotFoundException
* if either provided {@link DistributionSet} or {@link Target}s
* do not exist
*
* @throws QuotaExceededException
* if the maximum number of targets the distribution set can be
* assigned to at once is exceeded
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
DistributionSetAssignmentResult assignDistributionSet(long dsID,
@NotEmpty Collection<TargetWithActionType> targets);
/**
* Assigns the given set of {@link DistributionSet} entities to the given
* {@link Target}s using the specified {@link ActionType} and
* {@code forcetime}.
*
* @param dsIDs
* the set of IDs of the distribution sets to assign
* @param targets
* a list of all targets and their action type
* @return the list of assignment results
*
* @throws IncompleteDistributionSetException
@@ -126,26 +69,29 @@ public interface DeploymentManagement {
* @throws QuotaExceededException
* if the maximum number of targets the distribution set can be
* assigned to at once is exceeded
* @throws MultiAssignmentIsNotEnabledException
* if the request results in multiple assignments to the same
* target and multiassignment is disabled
*
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
List<DistributionSetAssignmentResult> assignDistributionSets(@NotEmpty Set<Long> dsIDs,
@NotEmpty Collection<TargetWithActionType> targets);
List<DistributionSetAssignmentResult> assignDistributionSets(
@NotEmpty List<DeploymentRequest> deploymentRequests);
/**
* Assigns the addressed {@link DistributionSet} to all {@link Target}s by
* their IDs with a specific {@link ActionType} and an action message.
* Assigns {@link DistributionSet}s to {@link Target}s according to the
* {@link DeploymentRequest}.
*
* @param dsID
* the ID of the distribution set to assign
* @param targets
* a list of all targets and their action type
* @param deploymentRequests
* information about all target-ds-assignments that shall be made
* @param actionMessage
* an optional message for the action status
* @return the assignment result
*
* @return the list of assignment results
*
* @throws IncompleteDistributionSetException
* if mandatory {@link SoftwareModuleType} are not assigned as
* define by the {@link DistributionSetType}.
* defined by the {@link DistributionSetType}.
*
* @throws EntityNotFoundException
* if either provided {@link DistributionSet} or {@link Target}s
@@ -154,14 +100,32 @@ public interface DeploymentManagement {
* @throws QuotaExceededException
* if the maximum number of targets the distribution set can be
* assigned to at once is exceeded
* @throws MultiAssignmentIsNotEnabledException
* if the request results in multiple assignments to the same
* target and multiassignment is disabled
*
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
DistributionSetAssignmentResult assignDistributionSet(long dsID, @NotEmpty Collection<TargetWithActionType> targets,
String actionMessage);
List<DistributionSetAssignmentResult> assignDistributionSets(
@NotEmpty List<DeploymentRequest> deploymentRequests, String actionMessage);
/**
* Registers an "offline" assignment, i.e. adds a completed action for the
* given {@link DistributionSet} to the given {@link Target}s.
* build a {@link DeploymentRequest} for a target distribution set
* assignment
*
* @param controllerId
* ID of target
* @param distributionSetId
* ID of distribution set
* @return the builder
*/
static DeploymentRequestBuilder deploymentRequest(final String controllerId, final long distributionSetId) {
return new DeploymentRequestBuilder(controllerId, distributionSetId);
}
/**
* Registers "offline" assignments. "offline" assignment means adding a
* completed action for a {@link DistributionSet} to a {@link Target}.
*
* The handling differs to hawkBit-managed updates by means that:<br/>
*
@@ -174,11 +138,10 @@ public interface DeploymentManagement {
* <li>does not send a {@link TargetAssignDistributionSetEvent}.</li>
* </ol>
*
* @param dsID
* the ID of the distribution set that was assigned
* @param controllerIDs
* a list of IDs of the targets that where assigned
* @return the assignment result
* @param assignments
* target IDs with the respective distribution set ID which they
* are supposed to be assigned to
* @return the assignment results
*
* @throws IncompleteDistributionSetException
* if mandatory {@link SoftwareModuleType} are not assigned as
@@ -191,9 +154,13 @@ public interface DeploymentManagement {
* @throws QuotaExceededException
* if the maximum number of targets the distribution set can be
* assigned to at once is exceeded
*
* @throws MultiAssignmentIsNotEnabledException
* if the request results in multiple assignments to the same
* target and multiassignment is disabled
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
DistributionSetAssignmentResult offlineAssignedDistributionSet(Long dsID, Collection<String> controllerIDs);
List<DistributionSetAssignmentResult> offlineAssignedDistributionSets(Collection<Entry<String, Long>> assignments);
/**
* Cancels the {@link Action} with the given ID. The method will immediately

View File

@@ -80,10 +80,10 @@ public interface QuotaManagement {
int getMaxTargetsPerRolloutGroup();
/**
* @return the maximum number of targets which for a manual distribution set
* assignment
* @return the maximum number of target distribution set assignments
* resulting from a manual assignment
*/
int getMaxTargetsPerManualAssignment();
int getMaxTargetDistributionSetAssignmentsPerManualAssignment();
/**
* @return the maximum number of targets for an automatic distribution set

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2019 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.exception;
import org.eclipse.hawkbit.exception.AbstractServerRtException;
import org.eclipse.hawkbit.exception.SpServerError;
/**
* This exception is thrown if an operation requires multiassignments, but the
* feature is not enabled.
*
*/
public class MultiAssignmentIsNotEnabledException extends AbstractServerRtException {
private static final long serialVersionUID = 1L;
private static final SpServerError THIS_ERROR = SpServerError.SP_MULTIASSIGNMENT_NOT_ENABLED;
/**
* Default constructor.
*/
public MultiAssignmentIsNotEnabledException() {
super(THIS_ERROR);
}
/**
* Parameterized constructor.
*
* @param cause
* of the exception
*/
public MultiAssignmentIsNotEnabledException(final Throwable cause) {
super(THIS_ERROR, cause);
}
/**
* Parameterized constructor.
*
* @param message
* of the exception
* @param cause
* of the exception
*/
public MultiAssignmentIsNotEnabledException(final String message, final Throwable cause) {
super(message, THIS_ERROR, cause);
}
/**
* Parameterized constructor.
*
* @param message
* of the exception
*/
public MultiAssignmentIsNotEnabledException(final String message) {
super(message, THIS_ERROR);
}
}

View File

@@ -120,7 +120,8 @@ public final class QuotaExceededException extends AbstractServerRtException {
* The maximum number of entities that can be assigned to the
* parent entity.
*/
public QuotaExceededException(final String type, final String parentType, final Long parentId, final long requested,
public QuotaExceededException(final String type, final String parentType, final Object parentId,
final long requested,
final long quota) {
super(String.format(ASSIGNMENT_QUOTA_EXCEEDED_MESSAGE, requested, type, parentType,
parentId != null ? String.valueOf(parentId) : "<new>", quota), SpServerError.SP_QUOTA_EXCEEDED);

View File

@@ -0,0 +1,105 @@
/**
* Copyright (c) 2019 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.model;
import java.util.Objects;
import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
/**
* A custom view on assigning a {@link DistributionSet} to a {@link Target}.
*
*/
public class DeploymentRequest {
private final Long distributionSetId;
private final TargetWithActionType targetWithActionType;
/**
* Constructor that also accepts maintenance schedule parameters and checks
* for validity of the specified maintenance schedule.
*
* @param controllerId
* for which the action is created.
* @param distributionSetId
* of the distribution set that that should be assigned to the
* controller.
* @param actionType
* specified for the action.
* @param forceTime
* at what time the type soft turns into forced.
* @param maintenanceSchedule
* is the cron expression to be used for scheduling maintenance
* windows. Expression has 6 mandatory fields and 1 last optional
* field: "second minute hour dayofmonth month weekday year"
* @param maintenanceWindowDuration
* in HH:mm:ss format specifying the duration of a maintenance
* 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.
*
* @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 String maintenanceSchedule, final String maintenanceWindowDuration,
final String maintenanceWindowTimeZone) {
this.targetWithActionType = new TargetWithActionType(controllerId, actionType, forceTime, maintenanceSchedule,
maintenanceWindowDuration,
maintenanceWindowTimeZone);
this.distributionSetId = distributionSetId;
}
public Long getDistributionSetId() {
return distributionSetId;
}
public String getControllerId() {
return targetWithActionType.getControllerId();
}
public TargetWithActionType getTargetWithActionType() {
return targetWithActionType;
}
@Override
public String toString() {
return String.format(
"DeploymentRequest [controllerId=%s, distributionSetId=%d, actionType=%s, forceTime=%d, maintenanceSchedule=%s, maintenanceWindowDuration=%s, maintenanceWindowTimeZone=%s]",
targetWithActionType.getControllerId(), getDistributionSetId(), targetWithActionType.getActionType(),
targetWithActionType.getForceTime(), targetWithActionType.getMaintenanceSchedule(),
targetWithActionType.getMaintenanceWindowDuration(),
targetWithActionType.getMaintenanceWindowTimeZone());
}
@Override
public int hashCode() {
return Objects.hash(distributionSetId, targetWithActionType);
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final DeploymentRequest other = (DeploymentRequest) obj;
return Objects.equals(distributionSetId, other.distributionSetId)
&& Objects.equals(targetWithActionType, other.targetWithActionType);
}
}

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2019 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.model;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
/**
* Builder for {@link DeploymentRequest}
*
*/
public class DeploymentRequestBuilder {
private final String controllerId;
private final Long distributionSetId;
private long forceTime = RepositoryModelConstants.NO_FORCE_TIME;
private ActionType actionType = ActionType.FORCED;
private String maintenanceSchedule;
private String maintenanceWindowDuration;
private String maintenanceWindowTimeZone;
/**
* Create a builder for a target distribution set assignment with the
* mandatory fields
*
* @param controllerId
* ID of the target
* @param distributionSetId
* ID of the distribution set
*/
public DeploymentRequestBuilder(final String controllerId, final Long distributionSetId) {
this.controllerId = controllerId;
this.distributionSetId = distributionSetId;
}
/**
* Set an other {@link ActionType} than {@link ActionType#FORCED}
*
* @param actionType
* type to used
* @return builder
*/
public DeploymentRequestBuilder setActionType(final ActionType actionType) {
this.actionType = actionType;
return this;
}
/**
* Set a forceTime other than the default one.
*
* @param forceTime
* at what time the type soft turns into forced.
* @return builder
*/
public DeploymentRequestBuilder setForceTime(final long forceTime) {
this.forceTime = forceTime;
return this;
}
/**
* Set a maintenanceWindow
*
* @param maintenanceSchedule
* is the cron expression to be used for scheduling maintenance
* windows. Expression has 6 mandatory fields and 1 last optional
* field: "second minute hour dayofmonth month weekday year"
* @param maintenanceWindowDuration
* in HH:mm:ss format specifying the duration of a maintenance
* 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.
* @return builder
*/
public DeploymentRequestBuilder setMaintenance(final String maintenanceSchedule,
final String maintenanceWindowDuration, final String maintenanceWindowTimeZone) {
this.maintenanceSchedule = maintenanceSchedule;
this.maintenanceWindowDuration = maintenanceWindowDuration;
this.maintenanceWindowTimeZone = maintenanceWindowTimeZone;
return this;
}
/**
* build the request
*
* @return the request object
*/
public DeploymentRequest build() {
return new DeploymentRequest(controllerId, distributionSetId, actionType, forceTime, maintenanceSchedule,
maintenanceWindowDuration, maintenanceWindowTimeZone);
}
}

View File

@@ -8,6 +8,8 @@
*/
package org.eclipse.hawkbit.repository.model;
import java.util.Objects;
import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
@@ -16,7 +18,6 @@ import org.eclipse.hawkbit.repository.model.Action.ActionType;
*
*/
public class TargetWithActionType {
private final String controllerId;
private final ActionType actionType;
private final long forceTime;
@@ -24,10 +25,27 @@ public class TargetWithActionType {
private String maintenanceWindowDuration;
private String maintenanceWindowTimeZone;
/**
* Constructor that uses {@link ActionType#FORCED}
*
* @param controllerId
* ID if the controller
*/
public TargetWithActionType(final String controllerId) {
this(controllerId, ActionType.FORCED, 0);
}
/**
* Constructor that leaves the maintenance info empty
*
* @param controllerId
* for which the action is created.
* @param actionType
* specified for the action.
* @param forceTime
* after that point in time the action is exposed as forcen in
* case the type is {@link ActionType#TIMEFORCED}
*/
public TargetWithActionType(final String controllerId, final ActionType actionType, final long forceTime) {
this.controllerId = controllerId;
this.actionType = actionType != null ? actionType : ActionType.FORCED;
@@ -42,6 +60,9 @@ public class TargetWithActionType {
* for which the action is created.
* @param actionType
* specified for the action.
* @param forceTime
* after that point in time the action is exposed as forcen in
* case the type is {@link ActionType#TIMEFORCED}
* @param maintenanceSchedule
* is the cron expression to be used for scheduling maintenance
* windows. Expression has 6 mandatory fields and 1 last optional
@@ -122,4 +143,30 @@ public class TargetWithActionType {
+ "]";
}
@Override
public int hashCode() {
return Objects.hash(actionType, controllerId, forceTime, maintenanceSchedule, maintenanceWindowDuration,
maintenanceWindowTimeZone);
}
@SuppressWarnings("squid:S1067")
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final TargetWithActionType other = (TargetWithActionType) obj;
return Objects.equals(actionType, other.actionType) && Objects.equals(controllerId, other.controllerId)
&& Objects.equals(forceTime, other.forceTime)
&& Objects.equals(maintenanceSchedule, other.maintenanceSchedule)
&& Objects.equals(maintenanceWindowDuration, other.maintenanceWindowDuration)
&& Objects.equals(maintenanceWindowTimeZone, other.maintenanceWindowTimeZone);
}
}

View File

@@ -89,8 +89,8 @@ public class PropertiesQuotaManagement implements QuotaManagement {
}
@Override
public int getMaxTargetsPerManualAssignment() {
return securityProperties.getDos().getMaxTargetsPerManualAssignment();
public int getMaxTargetDistributionSetAssignmentsPerManualAssignment() {
return securityProperties.getDos().getMaxTargetDistributionSetAssignmentsPerManualAssignment();
}
@Override

View File

@@ -10,9 +10,9 @@ package org.eclipse.hawkbit.repository.jpa;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.QuotaManagement;
@@ -51,17 +51,19 @@ public abstract class AbstractDsAssignmentStrategy {
protected final ActionRepository actionRepository;
private final ActionStatusRepository actionStatusRepository;
private final QuotaManagement quotaManagement;
private final BooleanSupplier multiAssignmentsConfig;
AbstractDsAssignmentStrategy(final TargetRepository targetRepository,
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement) {
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig) {
this.targetRepository = targetRepository;
this.afterCommit = afterCommit;
this.eventPublisherHolder = eventPublisherHolder;
this.actionRepository = actionRepository;
this.actionStatusRepository = actionStatusRepository;
this.quotaManagement = quotaManagement;
this.multiAssignmentsConfig = multiAssignmentsConfig;
}
/**
@@ -199,14 +201,14 @@ public abstract class AbstractDsAssignmentStrategy {
new CancelTargetAssignmentEvent(target, actionId, eventPublisherHolder.getApplicationId())));
}
JpaAction createTargetAction(final Map<String, TargetWithActionType> targetsWithActionMap, final JpaTarget target,
JpaAction createTargetAction(final TargetWithActionType targetWithActionType, final List<JpaTarget> targets,
final JpaDistributionSet set) {
// enforce the 'max actions per target' quota
assertActionsPerTargetQuota(target, 1);
final Optional<JpaTarget> optTarget = targets.stream()
.filter(t -> t.getControllerId().equals(targetWithActionType.getControllerId())).findFirst();
// create the action
return getTargetWithActionType(targetsWithActionMap, target.getControllerId()).map(targetWithActionType -> {
return optTarget.map(target -> {
assertActionsPerTargetQuota(target, 1);
final JpaAction actionForTarget = new JpaAction();
actionForTarget.setActionType(targetWithActionType.getActionType());
actionForTarget.setForcedTime(targetWithActionType.getForceTime());
@@ -218,7 +220,7 @@ public abstract class AbstractDsAssignmentStrategy {
actionForTarget.setMaintenanceWindowTimeZone(targetWithActionType.getMaintenanceWindowTimeZone());
return actionForTarget;
}).orElseGet(() -> {
LOG.warn("Cannot find targetWithActionType for target '{}'.", target.getControllerId());
LOG.warn("Cannot find target for targetWithActionType '{}'.", targetWithActionType.getControllerId());
return null;
});
}
@@ -241,14 +243,7 @@ public abstract class AbstractDsAssignmentStrategy {
actionRepository::countByTargetId);
}
private static Optional<TargetWithActionType> getTargetWithActionType(
final Map<String, TargetWithActionType> targetsWithActionMap, final String controllerId) {
if (targetsWithActionMap.containsKey(controllerId)) {
return Optional.of(targetsWithActionMap.get(controllerId));
} else {
return targetsWithActionMap.values().stream()
.filter(t -> controllerId.equalsIgnoreCase(t.getControllerId())).findFirst();
}
protected boolean isMultiAssignmentsEnabled() {
return multiAssignmentsConfig.getAsBoolean();
}
}

View File

@@ -18,6 +18,7 @@ import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -43,6 +44,7 @@ import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.ForceQuitActionNotAllowedException;
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
@@ -59,6 +61,7 @@ import org.eclipse.hawkbit.repository.model.Action;
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.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
@@ -81,8 +84,12 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@@ -131,6 +138,7 @@ public class JpaDeploymentManagement implements DeploymentManagement {
private final SystemSecurityContext systemSecurityContext;
private final TenantAware tenantAware;
private final Database database;
private final RetryTemplate retryTemplate;
protected JpaDeploymentManagement(final EntityManager entityManager, final ActionRepository actionRepository,
final DistributionSetRepository distributionSetRepository, final TargetRepository targetRepository,
@@ -150,80 +158,88 @@ public class JpaDeploymentManagement implements DeploymentManagement {
onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisherHolder,
actionRepository, actionStatusRepository, quotaManagement, this::isMultiAssignmentsEnabled);
offlineDsAssignmentStrategy = new OfflineDsAssignmentStrategy(targetRepository, afterCommit,
eventPublisherHolder, actionRepository, actionStatusRepository, quotaManagement);
eventPublisherHolder, actionRepository, actionStatusRepository, quotaManagement,
this::isMultiAssignmentsEnabled);
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.quotaManagement = quotaManagement;
this.systemSecurityContext = systemSecurityContext;
this.tenantAware = tenantAware;
this.database = database;
retryTemplate = createRetryTemplate();
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public DistributionSetAssignmentResult offlineAssignedDistributionSet(final Long dsID,
final Collection<String> controllerIDs) {
final DistributionSetAssignmentResult result = assignDistributionSetToTargets(dsID,
controllerIDs.stream()
.map(controllerId -> new TargetWithActionType(controllerId, ActionType.FORCED, -1))
.collect(Collectors.toList()),
null, offlineDsAssignmentStrategy);
offlineDsAssignmentStrategy.sendDeploymentEvents(result);
return result;
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public DistributionSetAssignmentResult assignDistributionSet(final long dsID, final ActionType actionType,
final long forcedTimestamp, final Collection<String> controllerIDs) {
final DistributionSetAssignmentResult result = assignDistributionSetToTargets(dsID,
controllerIDs.stream()
.map(controllerId -> new TargetWithActionType(controllerId, actionType, forcedTimestamp))
.collect(Collectors.toList()),
null, onlineDsAssignmentStrategy);
onlineDsAssignmentStrategy.sendDeploymentEvents(result);
return result;
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public DistributionSetAssignmentResult assignDistributionSet(final long dsID,
final Collection<TargetWithActionType> targets) {
final DistributionSetAssignmentResult result = assignDistributionSetToTargets(dsID, targets, null,
onlineDsAssignmentStrategy);
onlineDsAssignmentStrategy.sendDeploymentEvents(result);
return result;
}
@Override
public List<DistributionSetAssignmentResult> assignDistributionSets(final Set<Long> dsIDs,
final Collection<TargetWithActionType> targets) {
final List<DistributionSetAssignmentResult> results = dsIDs.stream()
.map(dsID -> assignDistributionSetToTargets(dsID, targets, null, onlineDsAssignmentStrategy))
public List<DistributionSetAssignmentResult> offlineAssignedDistributionSets(
final Collection<Entry<String, Long>> assignments) {
final Collection<Entry<String, Long>> distinctAssignments = assignments.stream().distinct()
.collect(Collectors.toList());
onlineDsAssignmentStrategy.sendDeploymentEvents(results);
enforceMaxAssignmentsPerRequest(distinctAssignments.size());
final List<DeploymentRequest> deploymentRequests = distinctAssignments.stream()
.map(entry -> DeploymentManagement.deploymentRequest(entry.getKey(), entry.getValue()).build())
.collect(Collectors.toList());
return assignDistributionSets(deploymentRequests, null, offlineDsAssignmentStrategy);
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
public List<DistributionSetAssignmentResult> assignDistributionSets(
final List<DeploymentRequest> deploymentRequests) {
return assignDistributionSets(deploymentRequests, null);
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
public List<DistributionSetAssignmentResult> assignDistributionSets(
final List<DeploymentRequest> deploymentRequests, final String actionMessage) {
return assignDistributionSets(deploymentRequests, actionMessage, onlineDsAssignmentStrategy);
}
private List<DistributionSetAssignmentResult> assignDistributionSets(
final List<DeploymentRequest> deploymentRequests, final String actionMessage,
final AbstractDsAssignmentStrategy strategy) {
final List<DeploymentRequest> validatedRequests = validateRequestForAssignments(deploymentRequests);
final Map<Long, List<TargetWithActionType>> assignmentsByDsIds = convertRequest(validatedRequests);
final List<DistributionSetAssignmentResult> results = assignmentsByDsIds.entrySet().stream()
.map(entry -> assignDistributionSetToTargetsWithRetry(entry.getKey(), entry.getValue(), actionMessage,
strategy))
.collect(Collectors.toList());
strategy.sendDeploymentEvents(results);
return results;
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public DistributionSetAssignmentResult assignDistributionSet(final long dsID,
final Collection<TargetWithActionType> targets, final String actionMessage) {
private List<DeploymentRequest> validateRequestForAssignments(List<DeploymentRequest> deploymentRequests) {
if (!isMultiAssignmentsEnabled()) {
deploymentRequests = deploymentRequests.stream().distinct().collect(Collectors.toList());
checkIfRequiresMultiAssignment(deploymentRequests);
}
checkQuotaForAssignment(deploymentRequests);
return deploymentRequests;
}
final DistributionSetAssignmentResult result = assignDistributionSetToTargets(dsID, targets, actionMessage,
onlineDsAssignmentStrategy);
onlineDsAssignmentStrategy.sendDeploymentEvents(result);
return result;
private static Map<Long, List<TargetWithActionType>> convertRequest(
final Collection<DeploymentRequest> deploymentRequests) {
return deploymentRequests.stream().collect(Collectors.groupingBy(DeploymentRequest::getDistributionSetId,
Collectors.mapping(DeploymentRequest::getTargetWithActionType, Collectors.toList())));
}
private static void checkIfRequiresMultiAssignment(final Collection<DeploymentRequest> deploymentRequests) {
final long distinctTargetsInRequest = deploymentRequests.stream()
.map(request -> request.getTargetWithActionType().getControllerId()).distinct().count();
if (distinctTargetsInRequest < deploymentRequests.size()) {
throw new MultiAssignmentIsNotEnabledException();
}
}
private DistributionSetAssignmentResult assignDistributionSetToTargetsWithRetry(final Long dsID,
final Collection<TargetWithActionType> targetsWithActionType, final String actionMessage,
final AbstractDsAssignmentStrategy assignmentStrategy) {
final RetryCallback<DistributionSetAssignmentResult, ConcurrencyFailureException> retryCallback = retryContext -> assignDistributionSetToTargets(
dsID, targetsWithActionType, actionMessage, assignmentStrategy);
return retryTemplate.execute(retryCallback);
}
/**
@@ -259,10 +275,10 @@ public class JpaDeploymentManagement implements DeploymentManagement {
final AbstractDsAssignmentStrategy assignmentStrategy) {
final JpaDistributionSet distributionSetEntity = getAndValidateDsById(dsID);
checkQuotaForAssignment(targetsWithActionType, distributionSetEntity);
final List<String> targetIds = targetsWithActionType.stream().map(TargetWithActionType::getControllerId).distinct()
.collect(Collectors.toList());
final List<JpaTarget> targetEntities = assignmentStrategy.findTargetsForAssignment(
targetsWithActionType.stream().map(TargetWithActionType::getControllerId).collect(Collectors.toList()),
final List<JpaTarget> targetEntities = assignmentStrategy.findTargetsForAssignment(targetIds,
distributionSetEntity.getId());
if (targetEntities.isEmpty()) {
@@ -288,7 +304,10 @@ public class JpaDeploymentManagement implements DeploymentManagement {
final AbstractDsAssignmentStrategy assignmentStrategy, final JpaDistributionSet distributionSetEntity,
final List<JpaTarget> targetEntities) {
final List<List<Long>> targetEntitiesIdsChunks = getTargetEntitiesAsChunks(targetEntities);
closeOrCancelActiveActions(assignmentStrategy, targetEntitiesIdsChunks);
if (!isMultiAssignmentsEnabled()) {
closeOrCancelActiveActions(assignmentStrategy, targetEntitiesIdsChunks);
}
// cancel all scheduled actions which are in-active, these actions were
// not active before and the manual assignment which has been done
// cancels them
@@ -320,7 +339,7 @@ public class JpaDeploymentManagement implements DeploymentManagement {
private static DistributionSetAssignmentResult buildAssignmentResult(final JpaDistributionSet distributionSet,
final List<JpaAction> assignedActions, final int totalTargetsForAssignment) {
int alreadyAssignedTargetsCount = totalTargetsForAssignment - assignedActions.size();
final int alreadyAssignedTargetsCount = totalTargetsForAssignment - assignedActions.size();
return new DistributionSetAssignmentResult(distributionSet, alreadyAssignedTargetsCount, assignedActions);
}
@@ -337,37 +356,31 @@ public class JpaDeploymentManagement implements DeploymentManagement {
return distributionSet;
}
private void checkQuotaForAssignment(final Collection<TargetWithActionType> targetsWithActionType,
final JpaDistributionSet distributionSet) {
// enforce the 'max targets per manual assignment' quota
if (!targetsWithActionType.isEmpty()) {
assertMaxTargetsPerManualAssignmentQuota(distributionSet.getId(), targetsWithActionType.size());
private void checkQuotaForAssignment(final Collection<DeploymentRequest> deploymentRequests) {
if (!deploymentRequests.isEmpty()) {
enforceMaxAssignmentsPerRequest(deploymentRequests.size());
enforceMaxActionsPerTarget(deploymentRequests);
}
}
/**
* Enforces the quota defining the maximum number of {@link Target}s per
* manual {@link DistributionSet} assignment.
*
* @param id
* of the distribution set
* @param requested
* number of targets to check
*/
private void assertMaxTargetsPerManualAssignmentQuota(final Long distributionSetId,
final int requestedTargetsCount) {
QuotaHelper.assertAssignmentQuota(distributionSetId, requestedTargetsCount,
quotaManagement.getMaxTargetsPerManualAssignment(), Target.class, DistributionSet.class, null);
private void enforceMaxAssignmentsPerRequest(final int requestedActions) {
QuotaHelper.assertAssignmentRequestSizeQuota(requestedActions,
quotaManagement.getMaxTargetDistributionSetAssignmentsPerManualAssignment());
}
private void enforceMaxActionsPerTarget(final Collection<DeploymentRequest> deploymentRequests) {
final int quota = quotaManagement.getMaxActionsPerTarget();
final Map<String, Long> countOfTargtInRequest = deploymentRequests.stream()
.map(DeploymentRequest::getControllerId)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
countOfTargtInRequest.forEach((controllerId, count) -> QuotaHelper.assertAssignmentQuota(controllerId, count,
quota, Action.class, Target.class, actionRepository::countByTargetControllerId));
}
private void closeOrCancelActiveActions(final AbstractDsAssignmentStrategy assignmentStrategy,
final List<List<Long>> targetIdsChunks) {
if (isMultiAssignmentsEnabled()) {
LOG.debug("Multi Assignments feature is enabled: No need to close /cancel active actions.");
return;
}
if (isActionsAutocloseEnabled()) {
assignmentStrategy.closeActiveActions(targetIdsChunks);
} else {
@@ -396,10 +409,8 @@ public class JpaDeploymentManagement implements DeploymentManagement {
private List<JpaAction> createActions(final Collection<TargetWithActionType> targetsWithActionType,
final List<JpaTarget> targets, final AbstractDsAssignmentStrategy assignmentStrategy,
final JpaDistributionSet set) {
final Map<String, TargetWithActionType> targetsWithActionMap = targetsWithActionType.stream()
.collect(Collectors.toMap(TargetWithActionType::getControllerId, Function.identity()));
return targets.stream().map(trg -> assignmentStrategy.createTargetAction(targetsWithActionMap, trg, set))
return targetsWithActionType.stream().map(twt -> assignmentStrategy.createTargetAction(twt, targets, set))
.filter(Objects::nonNull).map(actionRepository::save).collect(Collectors.toList());
}
@@ -820,4 +831,17 @@ public class JpaDeploymentManagement implements DeploymentManagement {
.runAsSystem(() -> tenantConfigurationManagement.getConfigurationValue(key, valueType).getValue());
}
private static RetryTemplate createRetryTemplate() {
final RetryTemplate template = new RetryTemplate();
final FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(Constants.TX_RT_DELAY);
template.setBackOffPolicy(backOffPolicy);
final SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(Constants.TX_RT_MAX,
Collections.singletonMap(ConcurrencyFailureException.class, true));
template.setRetryPolicy(retryPolicy);
return template;
}
}

View File

@@ -11,8 +11,9 @@ package org.eclipse.hawkbit.repository.jpa;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.QuotaManagement;
@@ -44,9 +45,9 @@ 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 QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig) {
super(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository,
quotaManagement);
quotaManagement, multiAssignmentsConfig);
}
@Override
@@ -59,10 +60,15 @@ public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
@Override
public List<JpaTarget> findTargetsForAssignment(final List<String> controllerIDs, final long setId) {
return Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream()
.map(ids -> targetRepository.findAll(SpecificationsBuilder.combineWithAnd(
Arrays.asList(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, setId),
TargetSpecifications.notEqualToTargetUpdateStatus(TargetUpdateStatus.PENDING)))))
final Function<List<String>, List<JpaTarget>> mapper;
if (isMultiAssignmentsEnabled()) {
mapper = targetRepository::findAllByControllerId;
} else {
mapper = ids -> targetRepository.findAll(SpecificationsBuilder.combineWithAnd(
Arrays.asList(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, setId),
TargetSpecifications.notEqualToTargetUpdateStatus(TargetUpdateStatus.PENDING))));
}
return Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream().map(mapper)
.flatMap(List::stream).collect(Collectors.toList());
}
@@ -84,9 +90,9 @@ public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
}
@Override
protected JpaAction createTargetAction(final Map<String, TargetWithActionType> targetsWithActionMap,
final JpaTarget target, final JpaDistributionSet set) {
final JpaAction result = super.createTargetAction(targetsWithActionMap, target, set);
protected JpaAction createTargetAction(final TargetWithActionType targetWithActionType,
final List<JpaTarget> targets, final JpaDistributionSet set) {
final JpaAction result = super.createTargetAction(targetWithActionType, targets, set);
if (result != null) {
result.setStatus(Status.FINISHED);
result.setActive(Boolean.FALSE);

View File

@@ -11,7 +11,6 @@ package org.eclipse.hawkbit.repository.jpa;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
@@ -46,15 +45,12 @@ import com.google.common.collect.Lists;
*/
public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
private final BooleanSupplier multiAssignmentsConfig;
OnlineDsAssignmentStrategy(final TargetRepository targetRepository,
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig) {
super(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository,
quotaManagement);
this.multiAssignmentsConfig = multiAssignmentsConfig;
quotaManagement, multiAssignmentsConfig);
}
@Override
@@ -130,9 +126,9 @@ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
}
@Override
JpaAction createTargetAction(final Map<String, TargetWithActionType> targetsWithActionMap, final JpaTarget target,
JpaAction createTargetAction(final TargetWithActionType targetWithActionType, final List<JpaTarget> targets,
final JpaDistributionSet set) {
final JpaAction result = super.createTargetAction(targetsWithActionMap, target, set);
final JpaAction result = super.createTargetAction(targetWithActionType, targets, set);
if (result != null) {
result.setStatus(Status.RUNNING);
}
@@ -207,10 +203,6 @@ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
.publishEvent(new MultiActionEvent(tenant, eventPublisherHolder.getApplicationId(), controllerIds)));
}
private boolean isMultiAssignmentsEnabled() {
return multiAssignmentsConfig.getAsBoolean();
}
private static Stream<Action> filterCancellations(final List<Action> actions) {
return actions.stream().filter(action -> {
final Status actionStatus = action.getStatus();

View File

@@ -19,11 +19,10 @@ import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.RepositoryModelConstants;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
@@ -143,11 +142,11 @@ public class AutoAssignChecker {
return DeploymentHelper.runInNewTransaction(transactionManager, "autoAssignDSToTargets",
Isolation.READ_COMMITTED.value(), status -> {
final List<TargetWithActionType> targets = getTargetsWithActionType(targetFilterQuery.getQuery(),
dsId, targetFilterQuery.getAutoAssignActionType(), PAGE_SIZE);
final int count = targets.size();
final List<DeploymentRequest> deploymentRequests = createAssignmentRequests(
targetFilterQuery.getQuery(), dsId, targetFilterQuery.getAutoAssignActionType(), PAGE_SIZE);
final int count = deploymentRequests.size();
if (count > 0) {
deploymentManagement.assignDistributionSet(dsId, targets, actionMessage);
deploymentManagement.assignDistributionSets(deploymentRequests, actionMessage);
}
return count;
});
@@ -168,7 +167,7 @@ public class AutoAssignChecker {
* maximum amount of targets to retrieve
* @return list of targets with action type
*/
private List<TargetWithActionType> getTargetsWithActionType(final String targetFilterQuery, final Long dsId,
private List<DeploymentRequest> createAssignmentRequests(final String targetFilterQuery, final Long dsId,
final ActionType type, final int count) {
final Page<Target> targets = targetManagement.findByTargetFilterQueryAndNonDS(PageRequest.of(0, count), dsId,
targetFilterQuery);
@@ -176,8 +175,10 @@ public class AutoAssignChecker {
// specified)
final ActionType autoAssignActionType = type == null ? ActionType.FORCED : type;
return targets.getContent().stream().map(t -> new TargetWithActionType(t.getControllerId(),
autoAssignActionType, RepositoryModelConstants.NO_FORCE_TIME)).collect(Collectors.toList());
return targets.getContent().stream()
.map(t -> DeploymentManagement.deploymentRequest(t.getControllerId(), dsId)
.setActionType(autoAssignActionType).build())
.collect(Collectors.toList());
}
}

View File

@@ -8,7 +8,7 @@
*/
package org.eclipse.hawkbit.repository.jpa.utils;
import java.util.function.Function;
import java.util.function.ToLongFunction;
import javax.validation.constraints.NotNull;
@@ -76,8 +76,8 @@ public final class QuotaHelper {
* if the assignment operation would cause the quota to be
* exceeded
*/
public static void assertAssignmentQuota(final Long parentId, final long requested, final long limit,
@NotNull final Class<?> type, @NotNull final Class<?> parentType, final Function<Long, Long> countFct) {
public static <T> void assertAssignmentQuota(final T parentId, final long requested, final long limit,
@NotNull final Class<?> type, @NotNull final Class<?> parentType, final ToLongFunction<T> countFct) {
assertAssignmentQuota(parentId, requested, limit, type.getSimpleName(), parentType.getSimpleName(), countFct);
}
@@ -104,8 +104,8 @@ public final class QuotaHelper {
* if the assignment operation would cause the quota to be
* exceeded
*/
public static void assertAssignmentQuota(final Long parentId, final long requested, final long limit,
@NotNull final String type, @NotNull final String parentType, final Function<Long, Long> countFct) {
public static <T> void assertAssignmentQuota(final T parentId, final long requested, final long limit,
@NotNull final String type, @NotNull final String parentType, final ToLongFunction<T> countFct) {
// check if the quota is unlimited
if (limit <= 0) {
@@ -121,7 +121,7 @@ public final class QuotaHelper {
}
if (parentId != null && countFct != null) {
final long currentCount = countFct.apply(parentId);
final long currentCount = countFct.applyAsLong(parentId);
if (currentCount + requested > limit) {
LOG.warn(
"Cannot assign {} {} entities to {} '{}' because of the configured quota limit {}. Currently, there are {} {} entities assigned.",
@@ -130,4 +130,22 @@ public final class QuotaHelper {
}
}
}
/**
* Assert that the number of assignments in a request does not exceed the
* limit.
*
* @param requested
* the number of assignments that are to be made
* @param limit
* the maximum number of assignments per request
*/
public static void assertAssignmentRequestSizeQuota(final long requested, final long limit) {
if (requested > limit) {
final String message = String.format(
"Quota exceeded: Cannot assign %s entities at once. The maximum is %s.", requested, limit);
LOG.warn(message);
throw new QuotaExceededException(message);
}
}
}

View File

@@ -55,7 +55,6 @@ import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException;
import org.eclipse.hawkbit.repository.exception.QuotaExceededException;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.Artifact;
@@ -1328,8 +1327,8 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest {
final String knownExternalref = "externalRefId" + i;
testdataFactory.createTarget(knownControllerId);
final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet(
knownDistributionSet.getId(), ActionType.FORCED, 0, Collections.singleton(knownControllerId));
final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(knownDistributionSet.getId(),
knownControllerId);
final Long actionId = getFirstAssignedActionId(assignmentResult);
controllerManagement.updateActionExternalRef(actionId, knownExternalref);

View File

@@ -13,15 +13,18 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.assertj.core.api.Assertions;
import org.eclipse.hawkbit.repository.ActionStatusFields;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.event.remote.MultiActionEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent;
@@ -35,6 +38,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.ForceQuitActionNotAllowedException;
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException;
import org.eclipse.hawkbit.repository.exception.QuotaExceededException;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
@@ -47,13 +51,13 @@ import org.eclipse.hawkbit.repository.model.Action;
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.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
import org.eclipse.hawkbit.repository.model.DistributionSetTag;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
@@ -100,12 +104,7 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
final Target target = testdataFactory.createTarget();
final String dsName = "DistributionSet";
verifyThrownExceptionBy(() -> deploymentManagement.assignDistributionSet(NOT_EXIST_IDL,
Collections.singletonList(new TargetWithActionType(target.getControllerId()))), dsName);
verifyThrownExceptionBy(() -> deploymentManagement.assignDistributionSet(NOT_EXIST_IDL,
Collections.singletonList(new TargetWithActionType(target.getControllerId())), "xxx"), dsName);
verifyThrownExceptionBy(() -> deploymentManagement.assignDistributionSet(NOT_EXIST_IDL, ActionType.FORCED,
System.currentTimeMillis(), Collections.singletonList(target.getControllerId())), dsName);
verifyThrownExceptionBy(() -> assignDistributionSet(NOT_EXIST_IDL, target.getControllerId()), dsName);
verifyThrownExceptionBy(() -> deploymentManagement.cancelAction(NOT_EXIST_IDL), "Action");
verifyThrownExceptionBy(() -> deploymentManagement.countActionsByTarget(NOT_EXIST_ID), "Target");
@@ -158,34 +157,35 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
}
@Test
@Description("Test verifies that the 'max actions per target' quota is enforced if the assigned distribution set is changed permanently.")
public void changeDistributionSetAssignmentUntilMaxActionsPerTargetQuotaIsExceeded() {
@Description("Test verifies that the 'max actions per target' quota is enforced.")
public void assertMaxActionsPerTargetQuotaIsEnforced() {
final int maxActions = quotaManagement.getMaxActionsPerTarget();
final List<Target> testTargets = testdataFactory.createTargets(1);
final Target testTarget = testdataFactory.createTarget();
final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1");
final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2");
final DistributionSet ds3 = testdataFactory.createDistributionSet("ds3");
IntStream.range(0, maxActions).forEach(i -> {
assignDistributionSet(i % 2 == 0 ? ds1 : ds2, testTargets);
});
enableMultiAssignments();
for (int i = 0; i < maxActions; i++) {
deploymentManagement.offlineAssignedDistributionSets(Collections
.singletonList(new SimpleEntry<String, Long>(testTarget.getControllerId(), ds1.getId())));
}
// change the distribution set one last time to trigger a quota hit
assertThatExceptionOfType(QuotaExceededException.class)
.isThrownBy(() -> assignDistributionSet(ds3, testTargets));
.isThrownBy(() -> assignDistributionSet(ds1, Collections.singletonList(testTarget)));
}
@Test
@Description("Assigns the same distribution set to many targets until the 'max targets per manual assignment' quota is exceeded.")
public void assignDistributionSetUntilQuotaIsExceeded() {
@Description("An assignment request with more assignments than allowed by 'maxTargetDistributionSetAssignmentsPerManualAssignment' quota throws an exception.")
public void assignmentRequestThatIsTooLarge() {
final int maxActions = quotaManagement.getMaxTargetDistributionSetAssignmentsPerManualAssignment();
final DistributionSet ds1 = testdataFactory.createDistributionSet("1");
final DistributionSet ds2 = testdataFactory.createDistributionSet("2");
final int maxTargets = quotaManagement.getMaxTargetsPerManualAssignment();
final DistributionSet ds = testdataFactory.createDistributionSet();
final List<Target> targets = testdataFactory.createTargets(maxActions, "assignmentTest1");
assignDistributionSet(ds1, targets);
assignDistributionSet(ds, testdataFactory.createTargets(maxTargets, "ok"));
assertThatExceptionOfType(QuotaExceededException.class)
.isThrownBy(() -> assignDistributionSet(ds, testdataFactory.createTargets(maxTargets + 1, "fail")));
targets.add(testdataFactory.createTarget("assignmentTest2"));
assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> assignDistributionSet(ds2, targets));
}
@Test
@@ -455,6 +455,7 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
@Expect(type = SoftwareModuleCreatedEvent.class, count = 6),
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1) })
public void assignedDistributionSet() {
final List<String> controllerIds = testdataFactory.createTargets(10).stream().map(Target::getControllerId)
.collect(Collectors.toList());
final List<Target> onlineAssignedTargets = testdataFactory.createTargets(10, "2");
@@ -464,8 +465,15 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
assignDistributionSet(testdataFactory.createDistributionSet("2"), onlineAssignedTargets);
final long current = System.currentTimeMillis();
final List<Target> targets = deploymentManagement.offlineAssignedDistributionSet(ds.getId(), controllerIds)
.getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());;
final List<Entry<String, Long>> offlineAssignments = controllerIds.stream()
.map(targetId -> new SimpleEntry<String, Long>(targetId, ds.getId())).collect(Collectors.toList());
final List<DistributionSetAssignmentResult> assignmentResults = deploymentManagement
.offlineAssignedDistributionSets(offlineAssignments);
assertThat(assignmentResults).hasSize(1);
final List<Target> targets = assignmentResults.get(0).getAssignedEntity().stream().map(Action::getTarget)
.collect(Collectors.toList());
assertThat(actionRepository.count()).isEqualTo(20);
assertThat(actionRepository.findByDistributionSetId(PAGE, ds.getId())).as("Offline actions are not active")
.allMatch(action -> !action.isActive());
@@ -480,6 +488,33 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
.allMatch(target -> target.getLastModifiedAt() == target.getInstallationDate());
}
@Test
@Description("Offline assign multiple DSs to multiple Targets in multiassignment mode.")
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 2),
@Expect(type = TargetUpdatedEvent.class, count = 4), @Expect(type = ActionCreatedEvent.class, count = 4),
@Expect(type = DistributionSetCreatedEvent.class, count = 2),
@Expect(type = SoftwareModuleCreatedEvent.class, count = 6) })
public void multiOfflineAssignment() {
final List<String> targetIds = testdataFactory.createTargets(2).stream().map(Target::getControllerId)
.collect(Collectors.toList());
final List<Long> dsIds = testdataFactory.createDistributionSets(2).stream().map(DistributionSet::getId)
.collect(Collectors.toList());
enableMultiAssignments();
final List<Entry<String, Long>> offlineAssignments = new ArrayList<>();
targetIds.forEach(targetId -> dsIds
.forEach(dsId -> offlineAssignments.add(new SimpleEntry<String, Long>(targetId, dsId))));
final List<DistributionSetAssignmentResult> assignmentResults = deploymentManagement
.offlineAssignedDistributionSets(offlineAssignments);
assertThat(getResultingActionCount(assignmentResults)).isEqualTo(4);
targetIds.forEach(controllerId -> {
final List<Long> assignedDsIds = actionRepository.findByTargetControllerId(PAGE, controllerId).stream()
.map(action -> action.getDistributionSet().getId()).collect(Collectors.toList());
assertThat(assignedDsIds).containsExactlyInAnyOrderElementsOf(dsIds);
});
}
@Test
@Description("Verifies that if an account is set to action autoclose running actions in case of a new assigned set get closed and set to CANCELED.")
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 10),
@@ -555,6 +590,92 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
assignment.stream().mapToLong(action -> action.getTarget().getId()).toArray());
}
@Test
@Description("Assign multiple DSs to multiple Targets in one request in multiassignment mode.")
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 2),
@Expect(type = TargetUpdatedEvent.class, count = 4), @Expect(type = ActionCreatedEvent.class, count = 4),
@Expect(type = DistributionSetCreatedEvent.class, count = 2),
@Expect(type = SoftwareModuleCreatedEvent.class, count = 6),
@Expect(type = MultiActionEvent.class, count = 1),
@Expect(type = TargetAssignDistributionSetEvent.class, count = 0) })
public void multiassignmentInOneRequest() {
final List<Target> targets = testdataFactory.createTargets(2);
final List<DistributionSet> distributionSets = testdataFactory.createDistributionSets(2);
final List<DeploymentRequest> deploymentRequests = createAssignmentRequests(distributionSets, targets);
enableMultiAssignments();
final List<DistributionSetAssignmentResult> results = deploymentManagement
.assignDistributionSets(deploymentRequests);
assertThat(getResultingActionCount(results)).isEqualTo(deploymentRequests.size());
final List<Long> dsIds = distributionSets.stream().map(DistributionSet::getId).collect(Collectors.toList());
targets.forEach(target -> {
final List<Long> assignedDsIds = actionRepository.findByTargetControllerId(PAGE, target.getControllerId())
.stream().map(action -> action.getDistributionSet().getId()).collect(Collectors.toList());
assertThat(assignedDsIds).containsExactlyInAnyOrderElementsOf(dsIds);
});
}
@Test
@Description("A Request resulting in multiple assignments to a single target is only allowed when multiassignment is enabled.")
public void multipleAssignmentsToTargetOnlyAllowedInMultiAssignMode() {
final Target target = testdataFactory.createTarget();
final List<DistributionSet> distributionSets = testdataFactory.createDistributionSets(2);
final DeploymentRequest targetToDS0 = DeploymentManagement
.deploymentRequest(target.getControllerId(), distributionSets.get(0).getId()).build();
final DeploymentRequest targetToDS1 = DeploymentManagement
.deploymentRequest(target.getControllerId(), distributionSets.get(1).getId()).build();
Assertions.assertThatExceptionOfType(MultiAssignmentIsNotEnabledException.class)
.isThrownBy(() -> deploymentManagement.assignDistributionSets(Arrays.asList(targetToDS0, targetToDS1)));
enableMultiAssignments();
assertThat(getResultingActionCount(
deploymentManagement.assignDistributionSets(Arrays.asList(targetToDS0, targetToDS1)))).isEqualTo(2);
}
@Test
@Description("Duplicate Assignments are removed from a request when multiassignment is disabled, otherwise not")
public void duplicateAssignmentsInRequestAreOnlyRemovedIfMultiassignmentDisabled() {
final Target target = testdataFactory.createTarget();
final DistributionSet ds = testdataFactory.createDistributionSet();
final List<DeploymentRequest> twoEqualAssignments = Collections.nCopies(2,
DeploymentManagement.deploymentRequest(target.getControllerId(), ds.getId()).build());
assertThat(getResultingActionCount(deploymentManagement.assignDistributionSets(twoEqualAssignments)))
.isEqualTo(1);
enableMultiAssignments();
assertThat(getResultingActionCount(deploymentManagement.assignDistributionSets(twoEqualAssignments)))
.isEqualTo(2);
}
private int getResultingActionCount(final List<DistributionSetAssignmentResult> results) {
return results.stream().map(DistributionSetAssignmentResult::getTotal).reduce(0, Integer::sum);
}
@Test
@Description("An assignment request is not accepted if it would lead to a target exceeding the max actions per target quota.")
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
@Expect(type = TargetAssignDistributionSetEvent.class, count = 0) })
public void maxActionsPerTargetIsCheckedBeforeAssignmentExecution() {
final int maxActions = quotaManagement.getMaxActionsPerTarget();
final String controllerId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
final List<DeploymentRequest> deploymentRequests = Collections.nCopies(maxActions + 1,
DeploymentManagement.deploymentRequest(controllerId, dsId).build());
enableMultiAssignments();
Assertions.assertThatExceptionOfType(QuotaExceededException.class)
.isThrownBy(() -> deploymentManagement.assignDistributionSets(deploymentRequests));
assertThat(actionRepository.countByTargetControllerId(controllerId)).isEqualTo(0);
}
/**
* test a simple deployment by calling the
* {@link TargetRepository#assignDistributionSet(DistributionSet, Iterable)}
@@ -563,11 +684,11 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
*/
@Test
@Description("Simple deployment or distribution set to target assignment test.")
@ExpectEvents({@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
@ExpectEvents({ @Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
@Expect(type = TargetCreatedEvent.class, count = 30), @Expect(type = ActionCreatedEvent.class, count = 20),
@Expect(type = TargetUpdatedEvent.class, count = 20),
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3)})
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3) })
public void assignDistributionSet2Targets() {
final String myCtrlIDPref = "myCtrlID";
@@ -617,7 +738,7 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
@Test
@Description("Test that it is not possible to assign a distribution set that is not complete.")
@ExpectEvents({@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
@ExpectEvents({ @Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
@Expect(type = TargetCreatedEvent.class, count = 10), @Expect(type = ActionCreatedEvent.class, count = 10),
@Expect(type = TargetUpdatedEvent.class, count = 10),
@@ -650,14 +771,14 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
@Test
@Description("Multiple deployments or distribution set to target assignment test. Expected behaviour is that a new deployment "
+ "overides unfinished old one which are canceled as part of the operation.")
@ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 5 + 4),
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 5 + 4),
@Expect(type = TargetUpdatedEvent.class, count = 3 * 4),
@Expect(type = ActionCreatedEvent.class, count = 3 * 4),
@Expect(type = ActionUpdatedEvent.class, count = 4 * 2),
@Expect(type = CancelTargetAssignmentEvent.class, count = 4 * 2),
@Expect(type = DistributionSetCreatedEvent.class, count = 3),
@Expect(type = SoftwareModuleCreatedEvent.class, count = 9),
@Expect(type = TargetAssignDistributionSetEvent.class, count = 2)})
@Expect(type = TargetAssignDistributionSetEvent.class, count = 2) })
public void mutipleDeployments() throws InterruptedException {
final String undeployedTargetPrefix = "undep-T";
final int noOfUndeployedTargets = 5;
@@ -982,10 +1103,8 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
final Target target = testdataFactory.createTarget("knownControllerId");
final DistributionSet ds = testdataFactory.createDistributionSet("a");
// assign ds to create an action
final DistributionSetAssignmentResult assignDistributionSet = deploymentManagement.assignDistributionSet(
ds.getId(), ActionType.SOFT,
org.eclipse.hawkbit.repository.model.RepositoryModelConstants.NO_FORCE_TIME,
Collections.singletonList(target.getControllerId()));
final DistributionSetAssignmentResult assignDistributionSet = assignDistributionSet(ds.getId(),
target.getControllerId(), ActionType.SOFT);
final Long actionId = getFirstAssignedActionId(assignDistributionSet);
// verify preparation
Action findAction = deploymentManagement.findAction(actionId).get();
@@ -1006,10 +1125,8 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
final Target target = testdataFactory.createTarget("knownControllerId");
final DistributionSet ds = testdataFactory.createDistributionSet("a");
// assign ds to create an action
final DistributionSetAssignmentResult assignDistributionSet = deploymentManagement.assignDistributionSet(
ds.getId(), ActionType.FORCED,
org.eclipse.hawkbit.repository.model.RepositoryModelConstants.NO_FORCE_TIME,
Collections.singletonList(target.getControllerId()));
final DistributionSetAssignmentResult assignDistributionSet = assignDistributionSet(ds.getId(),
target.getControllerId(), ActionType.FORCED);
final Long actionId = getFirstAssignedActionId(assignDistributionSet);
// verify perparation
Action findAction = deploymentManagement.findAction(actionId).get();
@@ -1023,23 +1140,22 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
findAction = deploymentManagement.findAction(actionId).get();
assertThat(findAction.getActionType()).as("action type is wrong").isEqualTo(ActionType.FORCED);
}
@Test
@Description("Tests the computation of already assigned entities returned as a result of an assignment")
public void testAlreadyAssignedAndAssignedActionsInAssignmentResult(){
// create target1, distributionSet, assign ds to target1 and finish update (close all actions)
final Action action = prepareFinishedUpdate("target1", "ds", false);
final Target target2 = testdataFactory.createTarget("target2");
final Target target2 = testdataFactory.createTarget("target2");
final Target target3 = testdataFactory.createTarget("target3");
// assign ds to target2, but don't finish update (actions should be still open)
assignDistributionSet(action.getDistributionSet().getId(), target2.getControllerId());
final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet(
final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(
action.getDistributionSet().getId(),
Arrays.asList(new TargetWithActionType(action.getTarget().getControllerId()),
new TargetWithActionType(target3.getControllerId())));
Arrays.asList(action.getTarget().getControllerId(), target3.getControllerId()), ActionType.FORCED);
assertThat(assignmentResult).isNotNull();
assertThat(assignmentResult.getTotal()).as("Total count of assigned and already assigned targets").isEqualTo(2);
assertThat(assignmentResult.getAssigned()).as("Total count of assigned targets").isEqualTo(1);
@@ -1170,8 +1286,4 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
}
private void enableMultiAssignments() {
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, true);
}
}

View File

@@ -13,7 +13,6 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -102,8 +101,8 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
final String knownControllerId = "controller12345";
final DistributionSet knownDistributionSet = testdataFactory.createDistributionSet();
testdataFactory.createTarget(knownControllerId);
final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet(
knownDistributionSet.getId(), ActionType.FORCED, 0, Collections.singleton(knownControllerId));
final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(knownDistributionSet.getId(),
knownControllerId);
final Long manuallyAssignedActionId = getFirstAssignedActionId(assignmentResult);
// create rollout with the same distribution set already assigned
@@ -142,8 +141,8 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
final DistributionSet firstDistributionSet = testdataFactory.createDistributionSet();
final DistributionSet secondDistributionSet = testdataFactory.createDistributionSet("second");
testdataFactory.createTarget(knownControllerId);
final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet(
firstDistributionSet.getId(), ActionType.FORCED, 0, Collections.singleton(knownControllerId));
final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(firstDistributionSet.getId(),
knownControllerId);
final Long manuallyAssignedActionId = getFirstAssignedActionId(assignmentResult);
// create rollout with the same distribution set already assigned

View File

@@ -11,7 +11,6 @@ package org.eclipse.hawkbit.repository.jpa.autoassign;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@@ -57,8 +56,8 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest {
final DistributionSet firstDistributionSet = testdataFactory.createDistributionSet();
final DistributionSet secondDistributionSet = testdataFactory.createDistributionSet("second");
testdataFactory.createTarget(knownControllerId);
final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet(
firstDistributionSet.getId(), ActionType.FORCED, 0, Collections.singleton(knownControllerId));
final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(firstDistributionSet.getId(),
knownControllerId);
final Long manuallyAssignedActionId = getFirstAssignedActionId(assignmentResult);
// target filter query that matches all targets
@@ -218,11 +217,11 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest {
final String targetDsBIdPref = "B";
final String targetDsCIdPref = "C";
List<Target> targetsA = createTargetsAndAutoAssignDistSet(targetDsAIdPref, 5, distributionSet,
final List<Target> targetsA = createTargetsAndAutoAssignDistSet(targetDsAIdPref, 5, distributionSet,
ActionType.FORCED);
List<Target> targetsB = createTargetsAndAutoAssignDistSet(targetDsBIdPref, 10, distributionSet,
final List<Target> targetsB = createTargetsAndAutoAssignDistSet(targetDsBIdPref, 10, distributionSet,
ActionType.SOFT);
List<Target> targetsC = createTargetsAndAutoAssignDistSet(targetDsCIdPref, 10, distributionSet,
final List<Target> targetsC = createTargetsAndAutoAssignDistSet(targetDsCIdPref, 10, distributionSet,
ActionType.DOWNLOAD_ONLY);
final int targetsCount = targetsA.size() + targetsB.size() + targetsC.size();

View File

@@ -14,12 +14,10 @@ import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationPrope
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.ACTION_CLEANUP_ENABLED;
import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Target;
@@ -54,10 +52,8 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1");
final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2");
deploymentManagement.assignDistributionSet(ds1.getId(), ActionType.FORCED, 0,
Collections.singletonList(trg1.getControllerId()));
deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0,
Collections.singletonList(trg2.getControllerId()));
assignDistributionSet(ds1.getId(), trg1.getControllerId());
assignDistributionSet(ds2.getId(), trg2.getControllerId());
assertThat(actionRepository.count()).isEqualTo(2);
@@ -80,10 +76,8 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1");
final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2");
final Long action1 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds1.getId(),
ActionType.FORCED, 0, Collections.singletonList(trg1.getControllerId())));
deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0,
Collections.singletonList(trg2.getControllerId()));
final Long action1 = getFirstAssignedActionId(assignDistributionSet(ds1.getId(), trg1.getControllerId()));
assignDistributionSet(ds2.getId(), trg2.getControllerId());
setActionToCanceled(action1);
@@ -109,12 +103,9 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1");
final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2");
final Long action1 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds1.getId(),
ActionType.FORCED, 0, Collections.singletonList(trg1.getControllerId())));
final Long action2 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds2.getId(),
ActionType.FORCED, 0, Collections.singletonList(trg2.getControllerId())));
final Long action3 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds2.getId(),
ActionType.FORCED, 0, Collections.singletonList(trg3.getControllerId())));
final Long action1 = getFirstAssignedActionId(assignDistributionSet(ds1.getId(), trg1.getControllerId()));
final Long action2 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), trg2.getControllerId()));
final Long action3 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), trg3.getControllerId()));
assertThat(actionRepository.count()).isEqualTo(3);
@@ -144,12 +135,9 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1");
final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2");
final Long action1 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds1.getId(),
ActionType.FORCED, 0, Collections.singletonList(trg1.getControllerId())));
final Long action2 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds2.getId(),
ActionType.FORCED, 0, Collections.singletonList(trg2.getControllerId())));
final Long action3 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds2.getId(),
ActionType.FORCED, 0, Collections.singletonList(trg3.getControllerId())));
final Long action1 = getFirstAssignedActionId(assignDistributionSet(ds1.getId(), trg1.getControllerId()));
final Long action2 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), trg2.getControllerId()));
final Long action3 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), trg3.getControllerId()));
assertThat(actionRepository.count()).isEqualTo(3);
@@ -181,12 +169,9 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1");
final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2");
final Long action1 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds1.getId(),
ActionType.FORCED, 0, Collections.singletonList(trg1.getControllerId())));
final Long action2 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds2.getId(),
ActionType.FORCED, 0, Collections.singletonList(trg2.getControllerId())));
final Long action3 = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(ds2.getId(),
ActionType.FORCED, 0, Collections.singletonList(trg3.getControllerId())));
final Long action1 = getFirstAssignedActionId(assignDistributionSet(ds1.getId(), trg1.getControllerId()));
final Long action2 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), trg2.getControllerId()));
final Long action3 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), trg3.getControllerId()));
assertThat(actionRepository.count()).isEqualTo(3);

View File

@@ -8,6 +8,7 @@
*/
package org.eclipse.hawkbit.repository.test.util;
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE;
import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.SYSTEM_ROLE;
@@ -15,7 +16,9 @@ import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -46,6 +49,7 @@ import org.eclipse.hawkbit.repository.TargetTagManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
import org.eclipse.hawkbit.repository.model.DistributionSetMetadata;
@@ -55,11 +59,11 @@ import org.eclipse.hawkbit.repository.model.RepositoryModelConstants;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetMetadata;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.eclipse.hawkbit.repository.test.TestConfiguration;
import org.eclipse.hawkbit.repository.test.matcher.EventVerifier;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
@@ -231,8 +235,42 @@ public abstract class AbstractIntegrationTest {
protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final String controllerId,
final ActionType actionType) {
return deploymentManagement.assignDistributionSet(dsID, Collections.singletonList(
new TargetWithActionType(controllerId, actionType, RepositoryModelConstants.NO_FORCE_TIME)));
return assignDistributionSet(dsID, Collections.singletonList(controllerId), actionType);
}
protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final String controllerId,
final ActionType actionType, final long forcedTime) {
return assignDistributionSet(dsID, Collections.singletonList(controllerId), actionType, forcedTime);
}
protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final List<String> controllerIds,
final ActionType actionType) {
return assignDistributionSet(dsID, controllerIds, actionType, RepositoryModelConstants.NO_FORCE_TIME);
}
protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final List<String> controllerIds,
final ActionType actionType, final long forcedTime) {
final List<DeploymentRequest> deploymentRequests = controllerIds.stream()
.map(id -> DeploymentManagement.deploymentRequest(id, dsID).setActionType(actionType)
.setForceTime(forcedTime).build())
.collect(Collectors.toList());
final List<DistributionSetAssignmentResult> results = deploymentManagement
.assignDistributionSets(deploymentRequests);
assertThat(results).hasSize(1);
return results.get(0);
}
protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet ds,
final List<Target> targets) {
final List<String> targetIds = targets.stream().map(Target::getControllerId).collect(Collectors.toList());
return assignDistributionSet(ds.getId(), targetIds, ActionType.FORCED);
}
private DistributionSetAssignmentResult makeAssignment(final DeploymentRequest request) {
final List<DistributionSetAssignmentResult> results = deploymentManagement
.assignDistributionSets(Collections.singletonList(request));
assertThat(results).hasSize(1);
return results.get(0);
}
/**
@@ -264,34 +302,37 @@ public abstract class AbstractIntegrationTest {
protected DistributionSetAssignmentResult assignDistributionSetWithMaintenanceWindow(final long dsID,
final String controllerId, final String maintenanceWindowSchedule, final String maintenanceWindowDuration,
final String maintenanceWindowTimeZone) {
return deploymentManagement.assignDistributionSet(dsID,
Arrays.asList(new TargetWithActionType(controllerId, ActionType.FORCED,
RepositoryModelConstants.NO_FORCE_TIME, maintenanceWindowSchedule, maintenanceWindowDuration,
maintenanceWindowTimeZone)));
return makeAssignment(DeploymentManagement.deploymentRequest(controllerId, dsID)
.setMaintenance(maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone)
.build());
}
protected DistributionSetAssignmentResult assignDistributionSetWithMaintenanceWindowTimeForced(final long dsID,
final String controllerId, final String maintenanceWindowSchedule, final String maintenanceWindowDuration,
protected DistributionSetAssignmentResult assignDistributionSetWithMaintenanceWindow(final long dsID,
final String controllerId, final ActionType type, final String maintenanceWindowSchedule,
final String maintenanceWindowDuration,
final String maintenanceWindowTimeZone) {
return deploymentManagement.assignDistributionSet(dsID,
Arrays.asList(new TargetWithActionType(controllerId, ActionType.TIMEFORCED, System.currentTimeMillis(),
maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone)));
return makeAssignment(DeploymentManagement.deploymentRequest(controllerId, dsID).setActionType(type)
.setMaintenance(maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone)
.build());
}
protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet pset,
final List<Target> targets) {
return deploymentManagement.assignDistributionSet(pset.getId(),
targets.stream().map(Target::getTargetWithActionType).collect(Collectors.toList()));
protected List<DeploymentRequest> createAssignmentRequests(final Collection<DistributionSet> distributionSets,
final Collection<Target> targets) {
final List<DeploymentRequest> deploymentRequests = new ArrayList<>();
distributionSets.forEach(ds -> targets.forEach(
target -> deploymentRequests
.add(DeploymentManagement.deploymentRequest(target.getControllerId(), ds.getId()).build()))
);
return deploymentRequests;
}
protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet pset, final Target target) {
return assignDistributionSet(pset, Arrays.asList(target));
}
protected DistributionSetAssignmentResult assignDistributionSetTimeForced(final DistributionSet pset,
final Target target) {
return deploymentManagement.assignDistributionSet(pset.getId(), Arrays.asList(
new TargetWithActionType(target.getControllerId(), ActionType.TIMEFORCED, System.currentTimeMillis())));
protected void enableMultiAssignments() {
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, true);
}
protected DistributionSetMetadata createDistributionSetMetadata(final long dsId, final MetaData md) {

View File

@@ -42,7 +42,6 @@ import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.Artifact;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.RepositoryModelConstants;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.test.matcher.Expect;
@@ -154,10 +153,9 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest {
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
final Target savedTarget = createTargetAndAssertNoActiveActions();
final List<Target> targetsAssignedToDs = deploymentManagement
.assignDistributionSet(ds.getId(), ActionType.FORCED, RepositoryModelConstants.NO_FORCE_TIME,
Collections.singletonList(savedTarget.getControllerId()))
.getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());;
final List<Target> targetsAssignedToDs = assignDistributionSet(ds.getId(), savedTarget.getControllerId(),
ActionType.FORCED).getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
@@ -208,9 +206,8 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest {
final Target target = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
final Long actionId = getFirstAssignedActionId(
deploymentManagement.assignDistributionSet(ds.getId(), ActionType.TIMEFORCED,
System.currentTimeMillis() + 2_000, Collections.singletonList(target.getControllerId())));
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds.getId(), target.getControllerId(),
ActionType.TIMEFORCED, System.currentTimeMillis() + 2_000));
MvcResult mvcResult = performGet("/{tenant}/controller/v1/" + DEFAULT_CONTROLLER_ID, MediaTypes.HAL_JSON,
status().isOk(), tenantAware.getCurrentTenant()).andReturn();
@@ -257,8 +254,7 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest {
final Target savedTarget = createTargetAndAssertNoActiveActions();
final List<Target> saved = deploymentManagement.assignDistributionSet(ds.getId(), ActionType.SOFT,
RepositoryModelConstants.NO_FORCE_TIME, Collections.singletonList(savedTarget.getControllerId()))
final List<Target> saved = assignDistributionSet(ds.getId(), savedTarget.getControllerId(), ActionType.SOFT)
.getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
@@ -317,8 +313,8 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest {
final Target savedTarget = createTargetAndAssertNoActiveActions();
final List<Target> saved = deploymentManagement.assignDistributionSet(ds.getId(), ActionType.TIMEFORCED,
System.currentTimeMillis(), Collections.singletonList(savedTarget.getControllerId()))
final List<Target> saved = assignDistributionSet(ds.getId(), savedTarget.getControllerId(),
ActionType.TIMEFORCED)
.getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
@@ -385,8 +381,8 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest {
final Target savedTarget = createTargetAndAssertNoActiveActions();
final List<Target> saved = deploymentManagement.assignDistributionSet(ds.getId(), ActionType.DOWNLOAD_ONLY,
RepositoryModelConstants.NO_FORCE_TIME, Collections.singletonList(savedTarget.getControllerId()))
final List<Target> saved = assignDistributionSet(ds.getId(), savedTarget.getControllerId(),
ActionType.DOWNLOAD_ONLY)
.getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);

View File

@@ -8,8 +8,8 @@
*/
package org.eclipse.hawkbit.mgmt.json.model;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* A generic abstract rest model which contains only a ID for use-case e.g.
@@ -19,12 +19,28 @@ import com.fasterxml.jackson.annotation.JsonProperty;
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class MgmtId {
@JsonProperty
private Long id;
/**
* Constructor
*/
public MgmtId() {
}
/**
* @return the id
* Constructor
*
* @param id
* ID of object
*/
@JsonCreator
public MgmtId(final Long id) {
this.id = id;
}
/**
* @return the ID
*/
public Long getId() {
return id;
@@ -32,7 +48,7 @@ public class MgmtId {
/**
* @param id
* the id to set
* the ID to set
*/
public void setId(final Long id) {
this.id = id;

View File

@@ -10,6 +10,7 @@ package org.eclipse.hawkbit.mgmt.json.model.distributionset;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -20,18 +21,21 @@ import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class MgmtTargetAssignmentRequestBody {
@JsonProperty
private String id;
private long forcetime;
private MgmtActionType type;
private MgmtMaintenanceWindowRequestBody maintenanceWindow;
/**
* {@link MgmtMaintenanceWindowRequestBody} object containing schedule,
* duration and timezone.
* JsonCreator Constructor
*
* @param id
* Mandatory ID of the target that should be assigned
*/
private MgmtMaintenanceWindowRequestBody maintenanceWindow;
@JsonCreator
public MgmtTargetAssignmentRequestBody(@JsonProperty(required = true, value = "id") final String id) {
this.id = id;
}
public String getId() {
return id;

View File

@@ -18,5 +18,4 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class MgmtSoftwareModuleTypeAssigment extends MgmtId {
}

View File

@@ -7,19 +7,29 @@ import org.eclipse.hawkbit.mgmt.json.model.MgmtId;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Request Body of DistributionSet for assignment operations (ID only).
*
*/
public class MgmtDistributionSetAssignment extends MgmtId {
private long forcetime;
private MgmtActionType type;
private MgmtMaintenanceWindowRequestBody maintenanceWindow;
/**
* {@link MgmtMaintenanceWindowRequestBody} object defining a schedule,
* duration and timezone.
* Constructor
*
* @param id
* ID of object
*/
private MgmtMaintenanceWindowRequestBody maintenanceWindow;
@JsonCreator
public MgmtDistributionSetAssignment(@JsonProperty(required = true, value = "id") final Long id) {
super(id);
}
public MgmtActionType getType() {
return type;

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2019 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.mgmt.json.model.target;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
/**
* Class to hold multiple distribution set assignments. A JSON object
* representing a single {@link MgmtDistributionSetAssignment} can be
* deserialized to an object of this class.
*/
@JsonDeserialize(using = MgmtDistributionSetAssignmentsDeserializer.class)
public class MgmtDistributionSetAssignments extends ArrayList<MgmtDistributionSetAssignment> {
private static final long serialVersionUID = 1L;
/**
* Constructor for an object that contains no distribution set assignment
*
*/
public MgmtDistributionSetAssignments() {
super();
}
/**
* Constructor for an object that contains a single distribution set
* assignment
*
* @param assignment
* the assignment
*/
public MgmtDistributionSetAssignments(final MgmtDistributionSetAssignment assignment) {
super();
add(assignment);
}
/**
* Constructor for an object that contains multiple distribution set
* assignments
*
* @param assignments
* the assignments
*/
public MgmtDistributionSetAssignments(final List<MgmtDistributionSetAssignment> assignments) {
super(assignments);
}
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2019 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.mgmt.json.model.target;
import java.io.IOException;
import java.util.Arrays;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
/**
* Deserializes a single object or a List of
* {@link MgmtDistributionSetAssignment}s
*/
public class MgmtDistributionSetAssignmentsDeserializer extends StdDeserializer<MgmtDistributionSetAssignments> {
private static final long serialVersionUID = 1L;
/**
* Mandatory constructor
*/
public MgmtDistributionSetAssignmentsDeserializer() {
this(null);
}
protected MgmtDistributionSetAssignmentsDeserializer(final Class<?> vc) {
super(vc);
}
@Override
public MgmtDistributionSetAssignments deserialize(final JsonParser jp, final DeserializationContext ctx)
throws IOException {
final MgmtDistributionSetAssignments assignments = new MgmtDistributionSetAssignments();
final ObjectCodec codec = jp.getCodec();
final JsonNode node = codec.readTree(jp);
if (node.isArray()) {
assignments.addAll(Arrays.asList(codec.treeToValue(node, MgmtDistributionSetAssignment[].class)));
} else {
assignments.add(codec.treeToValue(node, MgmtDistributionSetAssignment.class));
}
return assignments;
}
}

View File

@@ -18,7 +18,7 @@ import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssignment;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssignments;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAttributes;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody;
@@ -261,8 +261,8 @@ public interface MgmtTargetRestApi {
*
* @param targetId
* of the target to change
* @param dsId
* of the distributionset that is to be assigned
* @param dsAssignments
* the requested Assignments that shall be made
* @param offline
* to <code>true</code> if update was executed offline, i.e. not
* managed by hawkBit.
@@ -275,7 +275,7 @@ public interface MgmtTargetRestApi {
MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE })
ResponseEntity<MgmtTargetAssignmentResponseBody> postAssignedDistributionSet(
@PathVariable("targetId") String targetId, MgmtDistributionSetAssignment dsId,
@PathVariable("targetId") String targetId, MgmtDistributionSetAssignments dsAssignments,
@RequestParam(value = "offline", required = false) boolean offline);
/**

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2019 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.mgmt.rest.resource;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssignment;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder;
/**
* A mapper for assignment requests
*/
public final class MgmtDeploymentRequestMapper {
private MgmtDeploymentRequestMapper() {
// Utility class
}
/**
* Convert assignment information to an {@link DeploymentRequest}
*
* @param dsAssignment
* DS assignment information
* @param targetId
* target to assign the DS to
* @return resulting {@link DeploymentRequest}
*/
public static DeploymentRequest createAssignmentRequest(final MgmtDistributionSetAssignment dsAssignment,
final String targetId) {
return createAssignmentRequest(targetId, dsAssignment.getId(), dsAssignment.getType(),
dsAssignment.getForcetime(), dsAssignment.getMaintenanceWindow());
}
/**
* Convert assignment information to an {@link DeploymentRequest}
*
* @param targetAssignment
* target assignment information
* @param dsId
* DS to assign the target to
* @return resulting {@link DeploymentRequest}
*/
public static DeploymentRequest createAssignmentRequest(final MgmtTargetAssignmentRequestBody targetAssignment,
final Long dsId) {
return createAssignmentRequest(targetAssignment.getId(), dsId, targetAssignment.getType(),
targetAssignment.getForcetime(), targetAssignment.getMaintenanceWindow());
}
private static DeploymentRequest createAssignmentRequest(final String targetId, final Long dsId,
final MgmtActionType type, final long forcetime, final MgmtMaintenanceWindowRequestBody maintenanceWindow) {
final DeploymentRequestBuilder request = DeploymentManagement.deploymentRequest(targetId, dsId)
.setActionType(MgmtRestModelMapper.convertActionType(type)).setForceTime(forcetime);
if (maintenanceWindow != null) {
final String cronSchedule = maintenanceWindow.getSchedule();
final String duration = maintenanceWindow.getDuration();
final String timezone = maintenanceWindow.getTimezone();
MaintenanceScheduleHelper.validateMaintenanceSchedule(cronSchedule, duration, timezone);
request.setMaintenance(cronSchedule, duration, timezone);
}
return request.build();
}
}

View File

@@ -144,6 +144,20 @@ public final class MgmtDistributionSetMapper {
return result;
}
static MgmtTargetAssignmentResponseBody toResponse(
final List<DistributionSetAssignmentResult> dsAssignmentResults) {
final MgmtTargetAssignmentResponseBody result = new MgmtTargetAssignmentResponseBody();
final int alreadyAssigned = dsAssignmentResults.stream()
.mapToInt(DistributionSetAssignmentResult::getAlreadyAssigned).sum();
final List<MgmtActionId> assignedActions = dsAssignmentResults.stream()
.flatMap(assignmentResult -> assignmentResult.getAssignedEntity().stream())
.map(action -> new MgmtActionId(action.getTarget().getControllerId(), action.getId()))
.collect(Collectors.toList());
result.setAlreadyAssigned(alreadyAssigned);
result.setAssignedActions(assignedActions);
return result;
}
static List<MgmtDistributionSet> toResponseDistributionSets(final Collection<DistributionSet> sets) {
if (sets == null) {
return Collections.emptyList();

View File

@@ -8,11 +8,12 @@
*/
package org.eclipse.hawkbit.mgmt.rest.resource;
import java.util.AbstractMap.SimpleEntry;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
@@ -30,20 +31,19 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.SystemManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
import org.eclipse.hawkbit.repository.model.DistributionSetMetadata;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -250,33 +250,22 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
@PathVariable("distributionSetId") final Long distributionSetId,
@RequestBody final List<MgmtTargetAssignmentRequestBody> assignments,
@RequestParam(value = "offline", required = false) final boolean offline) {
if (offline) {
return ResponseEntity.ok(MgmtDistributionSetMapper
.toResponse(this.deployManagament.offlineAssignedDistributionSet(distributionSetId, assignments
.stream().map(MgmtTargetAssignmentRequestBody::getId).collect(Collectors.toList()))));
final List<Entry<String, Long>> offlineAssignments = assignments.stream()
.map(assignment -> new SimpleEntry<String, Long>(assignment.getId(), distributionSetId))
.collect(Collectors.toList());
return ResponseEntity
.ok(MgmtDistributionSetMapper
.toResponse(deployManagament.offlineAssignedDistributionSets(offlineAssignments)));
}
final List<DeploymentRequest> deploymentRequests = assignments.stream()
.map(assignment -> MgmtDeploymentRequestMapper.createAssignmentRequest(assignment, distributionSetId))
.collect(Collectors.toList());
final DistributionSetAssignmentResult assignDistributionSet = this.deployManagament
.assignDistributionSet(distributionSetId, assignments.stream().map(t -> {
final MgmtMaintenanceWindowRequestBody maintenanceWindow = t.getMaintenanceWindow();
if (maintenanceWindow == null) {
return new TargetWithActionType(t.getId(), MgmtRestModelMapper.convertActionType(t.getType()),
t.getForcetime());
}
final String cronSchedule = maintenanceWindow.getSchedule();
final String duration = maintenanceWindow.getDuration();
final String timezone = maintenanceWindow.getTimezone();
MaintenanceScheduleHelper.validateMaintenanceSchedule(cronSchedule, duration, timezone);
return new TargetWithActionType(t.getId(), MgmtRestModelMapper.convertActionType(t.getType()),
t.getForcetime(), cronSchedule, duration, timezone);
}).collect(Collectors.toList()));
return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(assignDistributionSet));
final List<DistributionSetAssignmentResult> assignmentResults = deployManagament
.assignDistributionSets(deploymentRequests);
return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(assignmentResults));
}

View File

@@ -8,14 +8,16 @@
*/
package org.eclipse.hawkbit.mgmt.rest.resource;
import java.util.AbstractMap.SimpleEntry;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import javax.validation.Valid;
import javax.validation.ValidationException;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
@@ -25,7 +27,7 @@ import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssignment;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssignments;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAttributes;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody;
@@ -33,15 +35,15 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetMetadata;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
@@ -281,36 +283,25 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
@Override
public ResponseEntity<MgmtTargetAssignmentResponseBody> postAssignedDistributionSet(
@PathVariable("targetId") final String targetId, @RequestBody final MgmtDistributionSetAssignment dsId,
@PathVariable("targetId") final String targetId,
@Valid @RequestBody final MgmtDistributionSetAssignments dsAssignments,
@RequestParam(value = "offline", required = false) final boolean offline) {
if (offline) {
final List<Entry<String, Long>> offlineAssignments = dsAssignments.stream()
.map(dsAssignment -> new SimpleEntry<String, Long>(targetId, dsAssignment.getId()))
.collect(Collectors.toList());
return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(
deploymentManagement.offlineAssignedDistributionSet(dsId.getId(),
Collections.singletonList(targetId))));
deploymentManagement.offlineAssignedDistributionSets(offlineAssignments)));
}
findTargetWithExceptionIfNotFound(targetId);
final MgmtMaintenanceWindowRequestBody maintenanceWindow = dsId.getMaintenanceWindow();
if (maintenanceWindow == null) {
return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(this.deploymentManagement
.assignDistributionSet(dsId.getId(), Collections.singletonList(new TargetWithActionType(targetId,
MgmtRestModelMapper.convertActionType(dsId.getType()), dsId.getForcetime())))));
}
final String cronSchedule = maintenanceWindow.getSchedule();
final String duration = maintenanceWindow.getDuration();
final String timezone = maintenanceWindow.getTimezone();
MaintenanceScheduleHelper.validateMaintenanceSchedule(cronSchedule, duration, timezone);
return ResponseEntity
.ok(MgmtDistributionSetMapper.toResponse(this.deploymentManagement.assignDistributionSet(dsId.getId(),
Collections.singletonList(new TargetWithActionType(targetId,
MgmtRestModelMapper.convertActionType(dsId.getType()), dsId.getForcetime(),
cronSchedule, duration, timezone)))));
final List<DeploymentRequest> deploymentRequests = dsAssignments.stream()
.map(dsAssignment -> MgmtDeploymentRequestMapper.createAssignmentRequest(dsAssignment, targetId))
.collect(Collectors.toList());
final List<DistributionSetAssignmentResult> assignmentResults = deploymentManagement
.assignDistributionSets(deploymentRequests);
return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(assignmentResults));
}
@Override

View File

@@ -27,10 +27,12 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.hawkbit.exception.SpServerError;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
import org.eclipse.hawkbit.repository.exception.QuotaExceededException;
import org.eclipse.hawkbit.repository.model.Action;
@@ -40,7 +42,6 @@ import org.eclipse.hawkbit.repository.model.DistributionSetMetadata;
import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
import org.eclipse.hawkbit.repository.test.util.WithUser;
import org.eclipse.hawkbit.rest.util.JsonBuilder;
@@ -299,11 +300,10 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr
}
@Test
@Description("Ensures that multi target assignment is protected by our 'max targets per manual assignment' quota.")
@Description("Ensures that multi target assignment is protected by our getMaxTargetDistributionSetAssignmentsPerManualAssignment quota.")
public void assignMultipleTargetsToDistributionSetUntilQuotaIsExceeded() throws Exception {
final int maxTargets = quotaManagement.getMaxTargetsPerManualAssignment();
final List<Target> targets = testdataFactory.createTargets(maxTargets + 1);
final int maxActions = quotaManagement.getMaxTargetDistributionSetAssignmentsPerManualAssignment();
final List<Target> targets = testdataFactory.createTargets(maxActions + 1);
final DistributionSet ds = testdataFactory.createDistributionSet();
final JSONArray payload = new JSONArray();
@@ -943,8 +943,7 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr
assertThat(distributionSetManagement.findByCompleted(PAGE, true)).hasSize(0);
final DistributionSet set = testdataFactory.createDistributionSet("one");
deploymentManagement.assignDistributionSet(set.getId(),
Arrays.asList(new TargetWithActionType(testdataFactory.createTarget().getControllerId())));
assignDistributionSet(set.getId(), testdataFactory.createTarget().getControllerId());
assertThat(distributionSetManagement.count()).isEqualTo(1);
@@ -1273,4 +1272,63 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr
.as("Five targets in repository have DS assigned").hasSize(5);
}
@Test
@Description("A request for assigning a target multiple times results in a Bad Request when multiassignment is disabled.")
public void multiassignmentRequestNotAllowedIfDisabled() throws Exception {
final String targetId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
final JSONArray body = new JSONArray();
body.put(getAssignmentObject(targetId, MgmtActionType.SOFT));
body.put(getAssignmentObject(targetId, MgmtActionType.FORCED));
mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", dsId).content(body.toString())
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
.andExpect(status().isBadRequest());
}
@Test
@Description("Identical assignments in a single request are removed when multiassignment is disabled.")
public void identicalAssignmentInRequestAreRemovedIfMultiassignmentsDisabled() throws Exception {
final String targetId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
final JSONArray body = new JSONArray();
body.put(getAssignmentObject(targetId, MgmtActionType.FORCED));
body.put(getAssignmentObject(targetId, MgmtActionType.FORCED));
mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", dsId).content(body.toString())
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andExpect(jsonPath("total", equalTo(1)));
}
@Test
@Description("Assigning targets multiple times to a DS in one request works in multiassignment mode.")
public void multiAssignment() throws Exception {
final List<String> targetIds = testdataFactory.createTargets(2).stream().map(Target::getControllerId)
.collect(Collectors.toList());
final Long dsId = testdataFactory.createDistributionSet().getId();
final JSONArray body = new JSONArray();
body.put(getAssignmentObject(targetIds.get(0), MgmtActionType.FORCED));
body.put(getAssignmentObject(targetIds.get(0), MgmtActionType.FORCED));
body.put(getAssignmentObject(targetIds.get(1), MgmtActionType.FORCED));
body.put(getAssignmentObject(targetIds.get(1), MgmtActionType.SOFT));
enableMultiAssignments();
mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", dsId).content(body.toString())
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andExpect(jsonPath("total", equalTo(body.length())));
}
public static JSONObject getAssignmentObject(final String targetId, final MgmtActionType type)
throws JSONException {
final JSONObject obj = new JSONObject();
obj.put("id", targetId);
obj.put("type", type.getName());
return obj;
}
}

View File

@@ -26,6 +26,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
@@ -60,6 +61,7 @@ import org.eclipse.hawkbit.rest.util.JsonBuilder;
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
import org.eclipse.hawkbit.util.IpUtil;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Test;
import org.springframework.data.domain.PageRequest;
@@ -1240,8 +1242,8 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest
public void updateAction() throws Exception {
final Target target = testdataFactory.createTarget();
final DistributionSet set = testdataFactory.createDistributionSet();
final Long actionId = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(set.getId(),
ActionType.SOFT, 0, Collections.singletonList(target.getControllerId())));
final Long actionId = getFirstAssignedActionId(
assignDistributionSet(set.getId(), target.getControllerId(), ActionType.SOFT));
assertThat(deploymentManagement.findAction(actionId).get().getActionType()).isEqualTo(ActionType.SOFT);
final String body = new JSONObject().put("forceType", "forced").toString();
@@ -1293,7 +1295,7 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest
@Description("Verfies that a DOWNLOAD_ONLY DS to target assignment is properly handled")
public void assignDownloadOnlyDistributionSetToTarget() throws Exception {
Target target = testdataFactory.createTarget();
final Target target = testdataFactory.createTarget();
final DistributionSet set = testdataFactory.createDistributionSet("one");
mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + "/assignedDS")
@@ -1303,7 +1305,7 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest
.andExpect(jsonPath("total", equalTo(1)));
assertThat(deploymentManagement.getAssignedDistributionSet(target.getControllerId()).get()).isEqualTo(set);
Slice<Action> actions = deploymentManagement.findActionsByTarget("targetExist", PageRequest.of(0, 100));
final Slice<Action> actions = deploymentManagement.findActionsByTarget("targetExist", PageRequest.of(0, 100));
assertThat(actions.getSize()).isGreaterThan(0);
actions.stream().filter(a -> a.getDistributionSet().equals(set))
.forEach(a -> ActionType.DOWNLOAD_ONLY.equals(a.getActionType()));
@@ -1866,4 +1868,69 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest
.andExpect(jsonPath("total", equalTo(1))).andExpect(jsonPath("content[0].key", equalTo("knownKey1")))
.andExpect(jsonPath("content[0].value", equalTo("knownValue1")));
}
@Test
@Description("A request for assigning multiple DS to a target results in a Bad Request when multiassignment in disabled.")
public void multiassignmentRequestNotAllowedIfDisabled() throws Exception {
final String targetId = testdataFactory.createTarget().getControllerId();
final List<Long> dsIds = testdataFactory.createDistributionSets(2).stream().map(DistributionSet::getId)
.collect(Collectors.toList());
final JSONArray body = getAssignmentBody(dsIds);
mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(body.toString())
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
.andExpect(status().isBadRequest());
}
@Test
@Description("Passing an array in assignment request is allowed if multiassignment is disabled and array size in 1.")
public void multiassignmentRequestAllowedIfDisabledButHasSizeOne() throws Exception {
final String targetId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
final JSONArray body = getAssignmentBody(Collections.singletonList(dsId));
mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(body.toString())
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk());
}
@Test
@Description("Identical assignments in a single request are removed when multiassignment in disabled.")
public void identicalAssignmentInRequestAreRemovedIfMultiassignmentsDisabled() throws Exception {
final String targetId = testdataFactory.createTarget().getControllerId();
final Long dsId = testdataFactory.createDistributionSet().getId();
final JSONArray body = getAssignmentBody(Arrays.asList(dsId, dsId));
mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(body.toString())
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andExpect(jsonPath("total", equalTo(1)));
}
@Test
@Description("Assign multiple DSs to a target in one request with multiassignments enabled.")
public void multiAssignment() throws Exception {
final String targetId = testdataFactory.createTarget().getControllerId();
final List<Long> dsIds = testdataFactory.createDistributionSets(2).stream().map(DistributionSet::getId)
.collect(Collectors.toList());
final JSONArray body = getAssignmentBody(dsIds);
enableMultiAssignments();
mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(body.toString())
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(2)));
}
public static JSONArray getAssignmentBody(final Collection<Long> dsIds) throws JSONException {
final JSONArray body = new JSONArray();
for (final Long id : dsIds) {
final JSONObject obj = new JSONObject();
obj.put("id", id);
body.put(obj);
}
return body;
}
}

View File

@@ -80,6 +80,7 @@ public class ResponseExceptionHandler {
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_AUTO_ASSIGN_ACTION_TYPE_INVALID, HttpStatus.BAD_REQUEST);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_AUTO_ASSIGN_DISTRIBUTION_SET_INVALID, HttpStatus.BAD_REQUEST);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED, HttpStatus.FORBIDDEN);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_MULTIASSIGNMENT_NOT_ENABLED, HttpStatus.BAD_REQUEST);
}
private static HttpStatus getStatusOrDefault(final SpServerError error) {

View File

@@ -547,13 +547,13 @@ include::../errors/429.adoc[]
|===
== POST /rest/v1/targets/{targetId}/assignedDS
== POST /rest/v1/targets/{targetId}/assignedDS (assign single distribution set)
=== Implementation Notes
Handles the POST request for assigning a distribution set to a specific target. Required Permission: READ_REPOSITORY and UPDATE_TARGET
=== Asssin distribution set to target
=== Assign distribution set to target
==== Curl
@@ -601,6 +601,60 @@ include::../errors/429.adoc[]
|===
== POST /rest/v1/targets/{targetId}/assignedDS (assign multiple distribution sets)
=== Implementation Notes
Handles the POST request for assigning multiple distribution sets to a specific target (only allowed id 'multi assignments' is enabled). Required Permission: READ_REPOSITORY and UPDATE_TARGET
=== Assign distribution sets to target
==== Curl
include::{snippets}/targets/post-assign-distribution-sets-to-target/curl-request.adoc[]
==== Request path parameter
include::{snippets}/targets/post-assign-distribution-sets-to-target/path-parameters.adoc[]
==== Request query parameter
include::{snippets}/targets/post-assign-distribution-sets-to-target/request-parameters.adoc[]
==== Request fields
include::{snippets}/targets/post-assign-distribution-sets-to-target/request-fields.adoc[]
==== Request URL
include::{snippets}/targets/post-assign-distribution-sets-to-target/http-request.adoc[]
=== Response (Status 200)
==== Response fields
include::{snippets}/targets/post-assign-distribution-sets-to-target/response-fields.adoc[]
==== Response example
include::{snippets}/targets/post-assign-distribution-sets-to-target/http-response.adoc[]
=== Error responses
|===
| HTTP Status Code | Reason | Response Model
include::../errors/400_multiassignment.adoc[]
include::../errors/401.adoc[]
include::../errors/403.adoc[]
include::../errors/404.adoc[]
include::../errors/405.adoc[]
include::../errors/406.adoc[]
include::../errors/409.adoc[]
include::../errors/415.adoc[]
include::../errors/429.adoc[]
|===
== GET /rest/v1/targets/{targetId}/attributes
=== Implementation Notes

View File

@@ -0,0 +1,3 @@
| `400 Bad Request`
| Bad Request - e.g. invalid parameters or 'multi assignments' is disabled
|

View File

@@ -21,7 +21,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.io.ByteArrayInputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@@ -73,7 +72,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
final DistributionSet set = testdataFactory.createDistributionSet("one");
final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID));
deploymentManagement.assignDistributionSet(set.getId(), Arrays.asList(target.getTargetWithActionType()));
assignDistributionSet(set.getId(), target.getControllerId());
mockMvc.perform(get(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}",
tenantAware.getCurrentTenant(), target.getControllerId()).accept(MediaTypes.HAL_JSON_VALUE))
@@ -101,8 +100,8 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
final DistributionSet setTwo = testdataFactory.createDistributionSet("two");
final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID));
deploymentManagement.assignDistributionSet(set.getId(), Arrays.asList(target.getTargetWithActionType()));
deploymentManagement.assignDistributionSet(setTwo.getId(), Arrays.asList(target.getTargetWithActionType()));
assignDistributionSet(set.getId(), target.getControllerId());
assignDistributionSet(setTwo.getId(), target.getControllerId());
mockMvc.perform(get(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}",
tenantAware.getCurrentTenant(), target.getControllerId()).accept(MediaTypes.HAL_JSON_VALUE))
@@ -137,8 +136,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
});
final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID));
final Long actionId = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(set.getId(),
Arrays.asList(target.getTargetWithActionType())));
final Long actionId = getFirstAssignedActionId(assignDistributionSet(set.getId(), target.getControllerId()));
final Action cancelAction = deploymentManagement.cancelAction(actionId);
mockMvc.perform(
@@ -169,8 +167,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
final DistributionSet set = testdataFactory.createDistributionSet("one");
final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID));
final Long actionId = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(set.getId(),
Arrays.asList(target.getTargetWithActionType())));
final Long actionId = getFirstAssignedActionId(assignDistributionSet(set.getId(), target.getControllerId()));
final Action cancelAction = deploymentManagement.cancelAction(actionId);
mockMvc.perform(post(
@@ -387,8 +384,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
final DistributionSet set = testdataFactory.createDistributionSet("one");
final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID));
final Long actionId = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(set.getId(),
Arrays.asList(target.getTargetWithActionType())));
final Long actionId = getFirstAssignedActionId(assignDistributionSet(set.getId(), target.getControllerId()));
mockMvc.perform(post(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
+ DdiRestConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}/feedback", tenantAware.getCurrentTenant(),
@@ -434,7 +430,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
.create(new ArtifactUpload(new ByteArrayInputStream(random), module.getId(), "binaryFile", false, 0));
final Target target = targetManagement.create(entityFactory.target().create().controllerId(CONTROLLER_ID));
deploymentManagement.assignDistributionSet(set.getId(), Arrays.asList(target.getTargetWithActionType()));
assignDistributionSet(set.getId(), target.getControllerId());
mockMvc.perform(
get(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/softwaremodules/{moduleId}/artifacts",

View File

@@ -27,6 +27,7 @@ import org.eclipse.hawkbit.ddi.rest.resource.DdiApiConfiguration;
import org.eclipse.hawkbit.mgmt.rest.resource.MgmtApiConfiguration;
import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.ArtifactUpload;
import org.eclipse.hawkbit.repository.model.DistributionSet;
@@ -169,7 +170,8 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati
private List<Target> assignWithoutMaintenanceWindow(final DistributionSet distributionSet, final Target savedTarget,
final boolean timeforced) {
final List<Action> actions = timeforced
? assignDistributionSetTimeForced(distributionSet, savedTarget).getAssignedEntity()
? assignDistributionSet(distributionSet.getId(), savedTarget.getControllerId(), ActionType.TIMEFORCED)
.getAssignedEntity()
: assignDistributionSet(distributionSet, savedTarget).getAssignedEntity();
return actions.stream().map(Action::getTarget).collect(Collectors.toList());
}
@@ -178,8 +180,8 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati
final boolean timeforced, final String maintenanceWindowSchedule, final String maintenanceWindowDuration,
final String maintenanceWindowTimeZone) {
final List<Action> actions = timeforced
? assignDistributionSetWithMaintenanceWindowTimeForced(distributionSet.getId(),
savedTarget.getControllerId(), maintenanceWindowSchedule, maintenanceWindowDuration,
? assignDistributionSetWithMaintenanceWindow(distributionSet.getId(), savedTarget.getControllerId(),
ActionType.TIMEFORCED, maintenanceWindowSchedule, maintenanceWindowDuration,
maintenanceWindowTimeZone).getAssignedEntity()
: assignDistributionSetWithMaintenanceWindow(distributionSet.getId(), savedTarget.getControllerId(),
maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone)

View File

@@ -188,7 +188,7 @@ public final class MgmtApiModelProperties {
// request parameter
public static final String FORCETIME = "Forcetime in milliseconds.";
public static final String FORCE = "Force as boolean.";
public static final String FORCETIME_TYPE = "The type of the forcetime.";
public static final String ASSIGNMENT_TYPE = "The type of the assignment.";
public static final String TARGET_ASSIGNED = "The number of targets that have been assigned as part of this operation.";
public static final String TARGET_ASSIGNED_ALREADY = "The number of targets which already had been the assignment.";
public static final String TARGET_ASSIGNED_TOTAL = "The total number of targets that are part of this operation.";

View File

@@ -383,17 +383,20 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat
parameterWithName("distributionSetId").description(ApiModelPropertiesGeneric.ITEM_ID)),
requestParameters(parameterWithName("offline")
.description(MgmtApiModelProperties.OFFLINE_UPDATE).optional()),
requestFields(requestFieldWithPath("[]forcetime").description(MgmtApiModelProperties.FORCETIME),
requestFieldWithPath("[]id").description(ApiModelPropertiesGeneric.ITEM_ID),
requestFieldWithPath("[]maintenanceWindow")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW).optional(),
requestFieldWithPath("[]maintenanceWindow.schedule")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_SCHEDULE).optional(),
requestFieldWithPath("[]maintenanceWindow.duration")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_DURATION).optional(),
requestFieldWithPath("[]maintenanceWindow.timezone")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE).optional(),
requestFieldWithPath("[]type").description(MgmtApiModelProperties.FORCETIME_TYPE)
requestFields(
requestFieldWithPath("[].id").description(ApiModelPropertiesGeneric.ITEM_ID),
optionalRequestFieldWithPath("[].forcetime")
.description(MgmtApiModelProperties.FORCETIME),
optionalRequestFieldWithPath("[].maintenanceWindow")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW),
optionalRequestFieldWithPath("[].maintenanceWindow.schedule")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_SCHEDULE),
optionalRequestFieldWithPath("[].maintenanceWindow.duration")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_DURATION),
optionalRequestFieldWithPath("[].maintenanceWindow.timezone")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE),
optionalRequestFieldWithPath("[].type")
.description(MgmtApiModelProperties.ASSIGNMENT_TYPE)
.attributes(key("value").value("['soft', 'forced','timeforced', 'downloadonly']"))),
responseFields(
fieldWithPath("assigned").description(MgmtApiModelProperties.DS_NEW_ASSIGNED_TARGETS),

View File

@@ -48,6 +48,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.payload.JsonFieldType;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -395,8 +396,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio
public void switchActionToForced() throws Exception {
final Target target = testdataFactory.createTarget(targetId);
final DistributionSet set = testdataFactory.createDistributionSet();
final Long actionId = getFirstAssignedActionId(deploymentManagement.assignDistributionSet(set.getId(),
ActionType.SOFT, 0, Collections.singletonList(target.getControllerId())));
final Long actionId = getFirstAssignedActionId(
assignDistributionSet(set.getId(), target.getControllerId(), ActionType.SOFT));
assertThat(deploymentManagement.findAction(actionId).get().getActionType()).isEqualTo(ActionType.SOFT);
final Map<String, Object> body = new HashMap<>();
@@ -485,7 +486,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio
pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)),
getResponseFieldsDistributionSet(false)));
}
@Test
@Description("Handles the POST request for assigning a distribution set to a specific target. Required Permission: READ_REPOSITORY and UPDATE_TARGET.")
public void postAssignDistributionSetToTarget() throws Exception {
@@ -511,30 +512,80 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio
pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)),
requestParameters(parameterWithName("offline")
.description(MgmtApiModelProperties.OFFLINE_UPDATE).optional()),
requestFields(requestFieldWithPath("forcetime").description(MgmtApiModelProperties.FORCETIME),
requestFields(
requestFieldWithPath("id").description(ApiModelPropertiesGeneric.ITEM_ID),
requestFieldWithPath("maintenanceWindow")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW).optional(),
requestFieldWithPath("maintenanceWindow.schedule")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_SCHEDULE).optional(),
requestFieldWithPath("maintenanceWindow.duration")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_DURATION).optional(),
requestFieldWithPath("maintenanceWindow.timezone")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE).optional(),
requestFieldWithPath("type").description(MgmtApiModelProperties.FORCETIME_TYPE)
optionalRequestFieldWithPath("forcetime").description(MgmtApiModelProperties.FORCETIME),
optionalRequestFieldWithPath("maintenanceWindow")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW),
optionalRequestFieldWithPath("maintenanceWindow.schedule")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_SCHEDULE),
optionalRequestFieldWithPath("maintenanceWindow.duration")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_DURATION),
optionalRequestFieldWithPath("maintenanceWindow.timezone")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE),
optionalRequestFieldWithPath("type").description(MgmtApiModelProperties.ASSIGNMENT_TYPE)
.attributes(key("value").value("['soft', 'forced','timeforced', 'downloadonly']"))),
responseFields(
fieldWithPath("assigned").description(MgmtApiModelProperties.DS_NEW_ASSIGNED_TARGETS),
fieldWithPath("alreadyAssigned").type(JsonFieldType.NUMBER)
.description(MgmtApiModelProperties.DS_ALREADY_ASSIGNED_TARGETS),
fieldWithPath("assignedActions").type(JsonFieldType.ARRAY)
.description(MgmtApiModelProperties.DS_NEW_ASSIGNED_ACTIONS),
fieldWithPath("assignedActions.[].id").type(JsonFieldType.NUMBER)
.description(MgmtApiModelProperties.ACTION_ID),
fieldWithPath("assignedActions.[]._links.self").type(JsonFieldType.OBJECT)
.description(MgmtApiModelProperties.LINK_TO_ACTION),
fieldWithPath("total").type(JsonFieldType.NUMBER)
.description(MgmtApiModelProperties.DS_TOTAL_ASSIGNED_TARGETS))));
responseFields(getDsAssignmentResponseFieldDescriptors())));
}
@Test
@Description("Handles the POST request for assigning distribution sets to a specific target. Required Permission: READ_REPOSITORY and UPDATE_TARGET.")
public void postAssignDistributionSetsToTarget() throws Exception {
// create target and ds, and assign ds
final List<DistributionSet> sets = testdataFactory.createDistributionSets(2);
testdataFactory.createTarget(targetId);
final long forceTime = System.currentTimeMillis();
final JSONArray body = new JSONArray();
body.put(new JSONObject().put("id", sets.get(1).getId()).put("type", "timeforced")
.put("forcetime", forceTime)
.put("maintenanceWindow", new JSONObject().put("schedule", getTestSchedule(100))
.put("duration", getTestDuration(10)).put("timezone", getTestTimeZone())))
.toString();
body.put(new JSONObject().put("id", sets.get(0).getId()).put("type", "forced"));
enableMultiAssignments();
mockMvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/"
+ MgmtRestConstants.TARGET_V1_ASSIGNED_DISTRIBUTION_SET, targetId).content(body.toString())
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andDo(this.document.document(
pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)),
requestParameters(parameterWithName("offline")
.description(MgmtApiModelProperties.OFFLINE_UPDATE).optional()),
requestFields(
requestFieldWithPath("[].id").description(ApiModelPropertiesGeneric.ITEM_ID),
optionalRequestFieldWithPath("[].forcetime")
.description(MgmtApiModelProperties.FORCETIME),
optionalRequestFieldWithPath("[].maintenanceWindow")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW),
optionalRequestFieldWithPath("[].maintenanceWindow.schedule")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_SCHEDULE),
optionalRequestFieldWithPath("[].maintenanceWindow.duration")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_DURATION),
optionalRequestFieldWithPath("[].maintenanceWindow.timezone")
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE),
optionalRequestFieldWithPath("[].type")
.description(MgmtApiModelProperties.ASSIGNMENT_TYPE)
.attributes(key("[].value")
.value("['soft', 'forced','timeforced', 'downloadonly']"))),
responseFields(getDsAssignmentResponseFieldDescriptors())));
}
private static FieldDescriptor[] getDsAssignmentResponseFieldDescriptors() {
final FieldDescriptor[] descriptors = {
fieldWithPath("assigned").description(MgmtApiModelProperties.DS_NEW_ASSIGNED_TARGETS),
fieldWithPath("alreadyAssigned").type(JsonFieldType.NUMBER)
.description(MgmtApiModelProperties.DS_ALREADY_ASSIGNED_TARGETS),
fieldWithPath("assignedActions").type(JsonFieldType.ARRAY)
.description(MgmtApiModelProperties.DS_NEW_ASSIGNED_ACTIONS),
fieldWithPath("assignedActions.[].id").type(JsonFieldType.NUMBER)
.description(MgmtApiModelProperties.ACTION_ID),
fieldWithPath("assignedActions.[]._links.self").type(JsonFieldType.OBJECT)
.description(MgmtApiModelProperties.LINK_TO_ACTION),
fieldWithPath("total").type(JsonFieldType.NUMBER)
.description(MgmtApiModelProperties.DS_TOTAL_ASSIGNED_TARGETS) };
return descriptors;
}
@Test

View File

@@ -245,15 +245,15 @@ public class HawkbitSecurityProperties {
private int maxTargetsPerRolloutGroup = 20000;
/**
* Maximum number of targets per rollout group
* Maximum number of overall actions targets per target
*/
private int maxActionsPerTarget = 2000;
/**
* Maximum number of targets for a manual distribution set assignment.
* Must be greater than 1000.
* Maximum number of actions resulting from a manual assignment of
* distribution sets and targets. Must be greater than 1000.
*/
private int maxTargetsPerManualAssignment = 5000;
private int maxTargetDistributionSetAssignmentsPerManualAssignment = 5000;
/**
* Maximum number of targets for an automatic distribution set
@@ -379,14 +379,15 @@ public class HawkbitSecurityProperties {
this.maxActionsPerTarget = maxActionsPerTarget;
}
public int getMaxTargetsPerManualAssignment() {
return maxTargetsPerManualAssignment;
public int getMaxTargetDistributionSetAssignmentsPerManualAssignment() {
return maxTargetDistributionSetAssignmentsPerManualAssignment;
}
public void setMaxTargetsPerManualAssignment(final int maxTargetsPerManualAssignment) {
this.maxTargetsPerManualAssignment = maxTargetsPerManualAssignment;
public void setMaxTargetDistributionSetAssignmentsPerManualAssignment(
final int maxTargetDistributionSetAssignmentsPerManualAssignment) {
this.maxTargetDistributionSetAssignmentsPerManualAssignment = maxTargetDistributionSetAssignmentsPerManualAssignment;
}
public int getMaxTargetsPerAutoAssignment() {
return maxTargetsPerAutoAssignment;
}

View File

@@ -8,6 +8,7 @@
*/
package org.eclipse.hawkbit.ui.management;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -17,12 +18,14 @@ import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException;
import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
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.RepositoryModelConstants;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.eclipse.hawkbit.ui.UiProperties;
import org.eclipse.hawkbit.ui.common.confirmwindow.layout.ConfirmationTab;
import org.eclipse.hawkbit.ui.common.entity.TargetIdName;
@@ -80,6 +83,7 @@ public final class TargetAssignmentOperations {
* the Vaadin Message Source for multi language
* @param eventSource
* the source object for sending potential events
* @throws MultiAssignmentIsNotEnabledException
*/
public static void saveAllAssignments(final List<Target> targets, final List<DistributionSet> distributionSets,
final ManagementUIState managementUIState,
@@ -101,31 +105,39 @@ public final class TargetAssignmentOperations {
final String maintenanceTimeZone = maintenanceWindowLayout.getMaintenanceTimeZone();
final Set<Long> dsIds = distributionSets.stream().map(DistributionSet::getId).collect(Collectors.toSet());
final List<TargetWithActionType> trgActionType = targets.stream()
.map(t -> maintenanceWindowLayout.isEnabled()
? new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp,
maintenanceSchedule, maintenanceDuration, maintenanceTimeZone)
: new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp))
.collect(Collectors.toList());
final List<DeploymentRequest> deploymentRequests = new ArrayList<>();
dsIds.forEach(dsId -> targets.forEach(t -> {
final DeploymentRequestBuilder request = DeploymentManagement.deploymentRequest(t.getControllerId(), dsId)
.setActionType(actionType).setForceTime(forcedTimeStamp);
if (maintenanceWindowLayout.isEnabled()) {
request.setMaintenance(maintenanceSchedule, maintenanceDuration, maintenanceTimeZone);
}
deploymentRequests.add(request.build());
}));
final List<DistributionSetAssignmentResult> results = deploymentManagement.assignDistributionSets(dsIds,
trgActionType);
try {
final List<DistributionSetAssignmentResult> results = deploymentManagement
.assignDistributionSets(deploymentRequests);
// use the last one for the notification box
final DistributionSetAssignmentResult assignmentResult = results.get(results.size() - 1);
if (assignmentResult.getAssigned() > 0) {
notification
.displaySuccess(i18n.getMessage("message.target.assignment", assignmentResult.getAssigned()));
}
if (assignmentResult.getAlreadyAssigned() > 0) {
notification.displaySuccess(
i18n.getMessage("message.target.alreadyAssigned", assignmentResult.getAlreadyAssigned()));
}
// use the last one for the notification box
final DistributionSetAssignmentResult assignmentResult = results.get(results.size() - 1);
if (assignmentResult.getAssigned() > 0) {
notification.displaySuccess(i18n.getMessage("message.target.assignment", assignmentResult.getAssigned()));
final Set<Long> targetIds = targets.stream().map(Target::getId).collect(Collectors.toSet());
refreshPinnedDetails(dsIds, targetIds, managementUIState, eventBus, eventSource);
notification.displaySuccess(i18n.getMessage("message.target.ds.assign.success"));
eventBus.publish(eventSource, SaveActionWindowEvent.SAVED_ASSIGNMENTS);
} catch (final MultiAssignmentIsNotEnabledException e) {
notification.displayValidationError(i18n.getMessage("message.target.ds.multiassign.error"));
LOG.error("UI allowed multiassignment although it is not enabled: {}", e);
}
if (assignmentResult.getAlreadyAssigned() > 0) {
notification.displaySuccess(
i18n.getMessage("message.target.alreadyAssigned", assignmentResult.getAlreadyAssigned()));
}
final Set<Long> targetIds = targets.stream().map(Target::getId).collect(Collectors.toSet());
refreshPinnedDetails(dsIds, targetIds, managementUIState, eventBus, eventSource);
notification.displaySuccess(i18n.getMessage("message.target.ds.assign.success"));
eventBus.publish(eventSource, SaveActionWindowEvent.SAVED_ASSIGNMENTS);
}
private static void refreshPinnedDetails(final Set<Long> dsIds, final Set<Long> targetIds,
@@ -275,4 +287,4 @@ public final class TargetAssignmentOperations {
return SPUIComponentProvider.getHelpLink(i18n, maintenanceWindowHelpUrl);
}
}
}

View File

@@ -24,6 +24,7 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
@@ -32,6 +33,7 @@ import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.TargetTagManagement;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.ui.common.tagdetails.TagData;
import org.eclipse.hawkbit.ui.components.HawkbitErrorNotificationMessage;
import org.eclipse.hawkbit.ui.management.event.BulkUploadValidationMessageEvent;
@@ -300,13 +302,17 @@ public class BulkUploadHandler extends CustomComponent
final ActionType actionType = ActionType.FORCED;
final long forcedTimeStamp = new Date().getTime();
final TargetBulkUpload targetBulkUpload = managementUIState.getTargetTableFilters().getBulkUpload();
final List<String> targetsList = targetBulkUpload.getTargetsCreated();
final List<String> targetIds = targetBulkUpload.getTargetsCreated();
final Long dsSelected = (Long) comboBox.getValue();
if (!distributionSetManagement.get(dsSelected).isPresent()) {
return i18n.getMessage("message.bulk.upload.assignment.failed");
}
deploymentManagement.assignDistributionSet(targetBulkUpload.getDsNameAndVersion(), actionType,
forcedTimeStamp, targetsList);
final List<DeploymentRequest> deploymentRequests = targetIds.stream()
.map(targetId -> DeploymentManagement
.deploymentRequest(targetId, targetBulkUpload.getDsNameAndVersion())
.setActionType(actionType).setForceTime(forcedTimeStamp).build())
.collect(Collectors.toList());
deploymentManagement.assignDistributionSets(deploymentRequests);
return null;
}

View File

@@ -516,6 +516,7 @@ message.dist.type.discard.success = All Distribution Types are discarded success
message.dist.discard.success = All Distributions are discarded successfully !
message.assign.discard.success = All assignments are discarded successfully !
message.target.ds.assign.success = Assignment saved successfully !
message.target.ds.multiassign.error = Cannot assign multiple distribution sets to a target!
message.bulk.upload.assignment.failed = Distribution set assignment failed as distribution set no longer exists!
message.bulk.upload.result.success = Successful: {0}
message.bulk.upload.result.fail = Failed: {0}