Adapt UI for target type compatibility check (#1189)

* Added compatibility calls needed for UI

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>

* Adapted UI for target type compatibility checks

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>

* improved exception handling for incompatibility check

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>

* added & fixed unit tests

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>

* fixed merged conflicts

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>

* fixed target type incompatibly specification

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>

* changed UI behaviour to close assignment popup in case of IncompatibleTargetTypeException

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>

* added unit test to validate incompatibly specification fix

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>

* fixed review findings

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>

* fixed review findings

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>

* fix potential null pointer

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>

* Fixed rolloutcopy by adding dsTypeId to ProxyDistributionSetInfo

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>

* suppressed warning

Signed-off-by: Robert Sing <robert.sing@bosch-si.com>
This commit is contained in:
Robert Sing
2021-10-22 16:23:25 +02:00
committed by GitHub
parent f94b4430e0
commit dea6fa3ce6
28 changed files with 473 additions and 347 deletions

View File

@@ -181,8 +181,8 @@ public interface RolloutManagement {
RolloutGroupConditions conditions);
/**
* Calculates how many targets are addressed by each rollout group and
* returns the validation information.
* Calculates how many targets are addressed by each rollout group and returns
* the validation information.
*
* @param groups
* a list of rollout groups
@@ -190,6 +190,8 @@ public interface RolloutManagement {
* the rollout
* @param createdAt
* timestamp when the rollout was created
* @param dsTypeId
* ID of the type of distribution set of the rollout
* @return the validation information
* @throws RolloutIllegalStateException
* thrown when no targets are targeted by the rollout
@@ -199,7 +201,7 @@ public interface RolloutManagement {
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ)
ListenableFuture<RolloutGroupsValidation> validateTargetsInGroups(@Valid List<RolloutGroupCreate> groups,
String targetFilter, Long createdAt);
String targetFilter, Long createdAt, @NotNull Long dsTypeId);
/**
* Retrieves all rollouts.

View File

@@ -102,8 +102,8 @@ public interface TargetManagement {
* @param selectTargetWithNoTag
* flag to select targets with no tag assigned
*
* @return the found number {@link Target}s
*
* @return the found number of {@link Target}s
*
* @throws EntityNotFoundException
* if distribution set with given ID does not exist
*/
@@ -117,7 +117,7 @@ public interface TargetManagement {
* @param distId
* to search for
* @return number of found {@link Target}s.
*
*
* @throws EntityNotFoundException
* if distribution set with given ID does not exist
*/
@@ -144,19 +144,33 @@ public interface TargetManagement {
*
* @param rsqlParam
* filter definition in RSQL syntax
* @return the found number {@link Target}s
* @return the found number of {@link Target}s
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET)
long countByRsql(@NotEmpty String rsqlParam);
/**
* Count all targets for given {@link TargetFilterQuery} and that are compatible
* with the passed {@link DistributionSetType}.
*
* @param rsqlParam
* filter definition in RSQL syntax
* @param dsTypeId
* ID of the {@link DistributionSetType} the targets need to be
* compatible with
* @return the found number of{@link Target}s
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET)
long countByRsqlAndCompatible(@NotEmpty String rsqlParam, @NotNull Long dsTypeId);
/**
* Count {@link TargetFilterQuery}s for given target filter query.
*
* @param targetFilterQueryId
* {@link TargetFilterQuery#getId()}
* @return the found number {@link Target}s
*
* @throws EntityNotFoundException
* @return the found number of {@link Target}s
*
* @throws EntityNotFoundException
* if {@link TargetFilterQuery} with given ID does not exist
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET)

View File

@@ -1,52 +0,0 @@
/**
* Copyright (c) 2021 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.exception;
import java.util.Collection;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.TargetType;
/**
* the {@link DistributionSetTypeNotInTargetTypeException} is thrown when a
* {@link DistributionSetType} is requested as part of a {@link TargetType} but
* is not returned in {@link TargetType#getCompatibleDistributionSetTypes()}.
*/
public class DistributionSetTypeNotInTargetTypeException extends EntityNotFoundException {
private static final long serialVersionUID = 1L;
/**
* Constructor
*
* @param distributionSetTypeId
* that is not compatible with given {@link TargetType}s
* @param targetTypeIds
* of the {@link TargetType}s where given {@link DistributionSetType}
* is not part of
*/
public DistributionSetTypeNotInTargetTypeException(final Long distributionSetTypeId,
final Collection<Long> targetTypeIds) {
super("DistributionSetType " + distributionSetTypeId + " is not compatible to TargetTypes " + targetTypeIds);
}
/**
* Constructor
*
* @param distributionSetTypeIds
* that are not compatible with given {@link TargetType}
* @param targetTypeId
* of the {@link TargetType} where given {@link DistributionSetType}s
* are not part of
*/
public DistributionSetTypeNotInTargetTypeException(final Collection<Long> distributionSetTypeIds,
final Long targetTypeId) {
super("DistributionSetTypes " + distributionSetTypeIds + " are not compatible to TargetTypes " + targetTypeId);
}
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2021 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.exception;
import java.util.Collection;
import java.util.Collections;
import org.eclipse.hawkbit.exception.AbstractServerRtException;
import org.eclipse.hawkbit.exception.SpServerError;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetType;
/**
* Thrown if user tries to assign a {@link DistributionSet} to a {@link Target}
* that has an incompatible {@link TargetType}
*/
public class IncompatibleTargetTypeException extends AbstractServerRtException {
private static final long serialVersionUID = 1L;
private final Collection<String> targetTypeNames;
private final Collection<String> distributionSetTypeNames;
/**
* Creates a new IncompatibleTargetTypeException with
* {@link SpServerError#SP_TARGET_TYPE_INCOMPATIBLE} error.
*
* @param targetTypeName
* Name of the target type
* @param distributionSetTypeNames
* Names of the distribution set types
*/
public IncompatibleTargetTypeException(final String targetTypeName,
final Collection<String> distributionSetTypeNames) {
super(String.format("Target of type %s is not compatible with distribution set of types %s", targetTypeName,
distributionSetTypeNames), SpServerError.SP_TARGET_TYPE_INCOMPATIBLE);
this.targetTypeNames = Collections.singleton(targetTypeName);
this.distributionSetTypeNames = distributionSetTypeNames;
}
/**
* Creates a new IncompatibleTargetTypeException with
* {@link SpServerError#SP_TARGET_TYPE_INCOMPATIBLE} error.
*
* @param targetTypeNames
* Name of the target types
* @param distributionSetTypeName
* Name of the distribution set type
*/
public IncompatibleTargetTypeException(final Collection<String> targetTypeNames,
final String distributionSetTypeName) {
super(String.format("Targets of types %s are not compatible with distribution set of type %s", targetTypeNames,
distributionSetTypeName), SpServerError.SP_TARGET_TYPE_INCOMPATIBLE);
this.targetTypeNames = targetTypeNames;
this.distributionSetTypeNames = Collections.singleton(distributionSetTypeName);
}
public Collection<String> getTargetTypeNames() {
return targetTypeNames;
}
public Collection<String> getDistributionSetTypeNames() {
return distributionSetTypeNames;
}
}

View File

@@ -1,168 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.validation.ConstraintDeclarationException;
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
import org.eclipse.hawkbit.repository.model.RolloutGroupsValidation;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.springframework.context.ApplicationContext;
import org.springframework.integration.support.locks.LockRegistry;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.concurrent.ListenableFuture;
/**
* Core functionality for {@link RolloutManagement} implementations.
*
*/
public abstract class AbstractRolloutManagement implements RolloutManagement {
protected final TargetManagement targetManagement;
protected final DeploymentManagement deploymentManagement;
protected final RolloutGroupManagement rolloutGroupManagement;
protected final DistributionSetManagement distributionSetManagement;
protected final ApplicationContext context;
protected final VirtualPropertyReplacer virtualPropertyReplacer;
protected final PlatformTransactionManager txManager;
protected final TenantAware tenantAware;
protected final LockRegistry lockRegistry;
protected final RolloutApprovalStrategy rolloutApprovalStrategy;
protected final TenantConfigurationManagement tenantConfigurationManagement;
protected final SystemSecurityContext systemSecurityContext;
protected AbstractRolloutManagement(final TargetManagement targetManagement,
final DeploymentManagement deploymentManagement, final RolloutGroupManagement rolloutGroupManagement,
final DistributionSetManagement distributionSetManagement, final ApplicationContext context,
final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager,
final TenantAware tenantAware, final LockRegistry lockRegistry,
final RolloutApprovalStrategy rolloutApprovalStrategy,
final TenantConfigurationManagement tenantConfigurationManagement,
final SystemSecurityContext systemSecurityContext) {
this.targetManagement = targetManagement;
this.deploymentManagement = deploymentManagement;
this.rolloutGroupManagement = rolloutGroupManagement;
this.distributionSetManagement = distributionSetManagement;
this.context = context;
this.virtualPropertyReplacer = virtualPropertyReplacer;
this.txManager = txManager;
this.tenantAware = tenantAware;
this.lockRegistry = lockRegistry;
this.rolloutApprovalStrategy = rolloutApprovalStrategy;
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.systemSecurityContext = systemSecurityContext;
}
protected RolloutGroupsValidation validateTargetsInGroups(final List<RolloutGroup> groups, final String baseFilter,
final long totalTargets) {
final List<Long> groupTargetCounts = new ArrayList<>(groups.size());
final Map<String, Long> targetFilterCounts = groups.stream()
.map(group -> RolloutHelper.getGroupTargetFilter(baseFilter, group)).distinct()
.collect(Collectors.toMap(Function.identity(), targetManagement::countByRsql));
long unusedTargetsCount = 0;
for (int i = 0; i < groups.size(); i++) {
final RolloutGroup group = groups.get(i);
final String groupTargetFilter = RolloutHelper.getGroupTargetFilter(baseFilter, group);
RolloutHelper.verifyRolloutGroupTargetPercentage(group.getTargetPercentage());
final long targetsInGroupFilter = targetFilterCounts.get(groupTargetFilter);
final long overlappingTargets = countOverlappingTargetsWithPreviousGroups(baseFilter, groups, group, i,
targetFilterCounts);
final long realTargetsInGroup;
// Assume that targets which were not used in the previous groups
// are used in this group
if (overlappingTargets > 0 && unusedTargetsCount > 0) {
realTargetsInGroup = targetsInGroupFilter - overlappingTargets + unusedTargetsCount;
unusedTargetsCount = 0;
} else {
realTargetsInGroup = targetsInGroupFilter - overlappingTargets;
}
final long reducedTargetsInGroup = Math
.round(group.getTargetPercentage() / 100 * (double) realTargetsInGroup);
groupTargetCounts.add(reducedTargetsInGroup);
unusedTargetsCount += realTargetsInGroup - reducedTargetsInGroup;
}
return new RolloutGroupsValidation(totalTargets, groupTargetCounts);
}
private long countOverlappingTargetsWithPreviousGroups(final String baseFilter, final List<RolloutGroup> groups,
final RolloutGroup group, final int groupIndex, final Map<String, Long> targetFilterCounts) {
// there can't be overlapping targets in the first group
if (groupIndex == 0) {
return 0;
}
final List<RolloutGroup> previousGroups = groups.subList(0, groupIndex);
final String overlappingTargetsFilter = RolloutHelper.getOverlappingWithGroupsTargetFilter(baseFilter,
previousGroups, group);
if (targetFilterCounts.containsKey(overlappingTargetsFilter)) {
return targetFilterCounts.get(overlappingTargetsFilter);
} else {
final long overlappingTargets = targetManagement.countByRsql(overlappingTargetsFilter);
targetFilterCounts.put(overlappingTargetsFilter, overlappingTargets);
return overlappingTargets;
}
}
protected long calculateRemainingTargets(final List<RolloutGroup> groups, final String targetFilter,
final Long createdAt) {
final String baseFilter = RolloutHelper.getTargetFilterQuery(targetFilter, createdAt);
final long totalTargets = targetManagement.countByRsql(baseFilter);
if (totalTargets == 0) {
throw new ConstraintDeclarationException("Rollout target filter does not match any targets");
}
final RolloutGroupsValidation validation = validateTargetsInGroups(groups, baseFilter, totalTargets);
return totalTargets - validation.getTargetsInGroups();
}
@Override
@Async
public ListenableFuture<RolloutGroupsValidation> validateTargetsInGroups(final List<RolloutGroupCreate> groups,
final String targetFilter, final Long createdAt) {
final String baseFilter = RolloutHelper.getTargetFilterQuery(targetFilter, createdAt);
final long totalTargets = targetManagement.countByRsql(baseFilter);
if (totalTargets == 0) {
throw new ConstraintDeclarationException("Rollout target filter does not match any targets");
}
return new AsyncResult<>(validateTargetsInGroups(
groups.stream().map(RolloutGroupCreate::build).collect(Collectors.toList()), baseFilter, totalTargets));
}
}

View File

@@ -45,8 +45,8 @@ public final class RolloutHelper {
}
/**
* Verifies that the group has the required success condition and action and
* a falid target percentage.
* Verifies that the group has the required success condition and action and a
* valid target percentage.
*
* @param group
* the input group
@@ -140,8 +140,8 @@ public final class RolloutHelper {
}
/**
* Filters the groups of a Rollout to match a specific status and adds a
* group to the result.
* Filters the groups of a Rollout to match a specific status and adds a group
* to the result.
*
* @param status
* the required status for the groups
@@ -156,8 +156,8 @@ public final class RolloutHelper {
}
/**
* Creates an RSQL expression that matches all targets in the provided
* groups. Links all target filter queries with OR.
* Creates an RSQL expression that matches all targets in the provided groups.
* Links all target filter queries with OR.
*
* @param groups
* the rollout groups
@@ -224,7 +224,7 @@ public final class RolloutHelper {
* group for which the filter string should be created
* @return the final target filter query for a rollout group
*/
static String getGroupTargetFilter(final String baseFilter, final RolloutGroup group) {
public static String getGroupTargetFilter(final String baseFilter, final RolloutGroup group) {
if (StringUtils.isEmpty(group.getTargetFilterQuery())) {
return baseFilter;
}

View File

@@ -42,9 +42,10 @@ import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
import org.eclipse.hawkbit.repository.exception.DistributionSetTypeNotInTargetTypeException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.ForceQuitActionNotAllowedException;
import org.eclipse.hawkbit.repository.exception.IncompatibleTargetTypeException;
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;
@@ -256,14 +257,14 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
private void checkCompatibilityForSingleDsAssignment(final Long distSetId, final List<String> controllerIds) {
final DistributionSetType distSetType = distributionSetManagement.getValidAndComplete(distSetId).getType();
final Set<Long> incompatibleTargetTypes = Lists.partition(controllerIds, Constants.MAX_ENTRIES_IN_STATEMENT)
final Set<String> incompatibleTargetTypes = Lists.partition(controllerIds, Constants.MAX_ENTRIES_IN_STATEMENT)
.stream()
.map(ids -> targetRepository.findAll(TargetSpecifications.hasControllerIdIn(ids)
.and(TargetSpecifications.notCompatibleWithDistributionSetType(distSetType.getId()))))
.flatMap(List::stream).map(Target::getTargetType).map(TargetType::getId).collect(Collectors.toSet());
.flatMap(List::stream).map(Target::getTargetType).map(TargetType::getName).collect(Collectors.toSet());
if (!incompatibleTargetTypes.isEmpty()) {
throw new DistributionSetTypeNotInTargetTypeException(distSetType.getId(), incompatibleTargetTypes);
throw new IncompatibleTargetTypeException(incompatibleTargetTypes, distSetType.getName());
}
}
@@ -278,9 +279,9 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
incompatibleDistSetTypes.removeAll(target.getTargetType().getCompatibleDistributionSetTypes());
if (!incompatibleDistSetTypes.isEmpty()) {
final Set<Long> distSetTypeIds = incompatibleDistSetTypes.stream().map(DistributionSetType::getId)
final Set<String> distSetTypeNames = incompatibleDistSetTypes.stream().map(DistributionSetType::getName)
.collect(Collectors.toSet());
throw new DistributionSetTypeNotInTargetTypeException(distSetTypeIds, target.getTargetType().getId());
throw new IncompatibleTargetTypeException(target.getTargetType().getName(), distSetTypeNames);
}
}
}

View File

@@ -10,25 +10,24 @@ package org.eclipse.hawkbit.repository.jpa;
import static org.eclipse.hawkbit.repository.jpa.builder.JpaRolloutGroupCreate.addSuccessAndErrorConditionsAndActions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.validation.ConstraintDeclarationException;
import javax.validation.ValidationException;
import org.eclipse.hawkbit.repository.AbstractRolloutManagement;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
import org.eclipse.hawkbit.repository.RolloutExecutor;
import org.eclipse.hawkbit.repository.RolloutFields;
import org.eclipse.hawkbit.repository.RolloutGroupManagement;
import org.eclipse.hawkbit.repository.RolloutHelper;
import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.RolloutStatusCache;
@@ -54,6 +53,7 @@ import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper;
import org.eclipse.hawkbit.repository.model.BaseEntity;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
@@ -70,7 +70,6 @@ import org.eclipse.hawkbit.tenancy.TenantAware;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -96,7 +95,7 @@ import com.google.common.collect.Lists;
*/
@Validated
@Transactional(readOnly = true)
public class JpaRolloutManagement extends AbstractRolloutManagement {
public class JpaRolloutManagement implements RolloutManagement {
private static final Logger LOGGER = LoggerFactory.getLogger(JpaRolloutManagement.class);
private static final List<RolloutStatus> ACTIVE_ROLLOUTS = Arrays.asList(RolloutStatus.CREATING,
@@ -125,23 +124,35 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
@Autowired
private RolloutStatusCache rolloutStatusCache;
private final TargetManagement targetManagement;
private final DistributionSetManagement distributionSetManagement;
private final VirtualPropertyReplacer virtualPropertyReplacer;
private final PlatformTransactionManager txManager;
private final TenantAware tenantAware;
private final LockRegistry lockRegistry;
private final RolloutApprovalStrategy rolloutApprovalStrategy;
private final TenantConfigurationManagement tenantConfigurationManagement;
private final SystemSecurityContext systemSecurityContext;
private final RolloutExecutor rolloutExecutor;
private final EventPublisherHolder eventPublisherHolder;
private final Database database;
public JpaRolloutManagement(final TargetManagement targetManagement,
final DeploymentManagement deploymentManagement, final RolloutGroupManagement rolloutGroupManagement,
final DistributionSetManagement distributionSetManagement, final ApplicationContext context,
final EventPublisherHolder eventPublisherHolder, final VirtualPropertyReplacer virtualPropertyReplacer,
final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry,
final Database database, final RolloutApprovalStrategy rolloutApprovalStrategy,
final DistributionSetManagement distributionSetManagement, final EventPublisherHolder eventPublisherHolder,
final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager,
final TenantAware tenantAware, final LockRegistry lockRegistry, final Database database,
final RolloutApprovalStrategy rolloutApprovalStrategy,
final TenantConfigurationManagement tenantConfigurationManagement,
final SystemSecurityContext systemSecurityContext, final RolloutExecutor rolloutExecutor) {
super(targetManagement, deploymentManagement, rolloutGroupManagement, distributionSetManagement, context,
virtualPropertyReplacer, txManager, tenantAware, lockRegistry, rolloutApprovalStrategy,
tenantConfigurationManagement, systemSecurityContext);
this.targetManagement = targetManagement;
this.distributionSetManagement = distributionSetManagement;
this.virtualPropertyReplacer = virtualPropertyReplacer;
this.txManager = txManager;
this.tenantAware = tenantAware;
this.lockRegistry = lockRegistry;
this.rolloutApprovalStrategy = rolloutApprovalStrategy;
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.systemSecurityContext = systemSecurityContext;
this.eventPublisherHolder = eventPublisherHolder;
this.database = database;
this.rolloutExecutor = rolloutExecutor;
@@ -203,7 +214,8 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
private JpaRollout createRollout(final JpaRollout rollout) {
WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(rollout);
final Long totalTargets = targetManagement.countByRsql(rollout.getTargetFilterQuery());
final Long totalTargets = targetManagement.countByRsqlAndCompatible(rollout.getTargetFilterQuery(),
rollout.getDistributionSet().getType().getId());
if (totalTargets == 0) {
throw new ValidationException("Rollout does not match any existing targets");
}
@@ -248,6 +260,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
final RolloutGroupConditions conditions, final Rollout rollout) {
RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING);
final JpaRollout savedRollout = (JpaRollout) rollout;
final DistributionSetType distributionSetType = savedRollout.getDistributionSet().getType();
// prepare the groups
final List<RolloutGroup> groups = groupList.stream()
@@ -255,13 +268,13 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
.collect(Collectors.toList());
groups.forEach(RolloutHelper::verifyRolloutGroupHasConditions);
RolloutHelper.verifyRemainingTargets(
calculateRemainingTargets(groups, savedRollout.getTargetFilterQuery(), savedRollout.getCreatedAt()));
RolloutHelper.verifyRemainingTargets(calculateRemainingTargets(groups, savedRollout.getTargetFilterQuery(),
savedRollout.getCreatedAt(), distributionSetType.getId()));
// check if we need to enforce the 'max targets per group' quota
if (quotaManagement.getMaxTargetsPerRolloutGroup() > 0) {
validateTargetsInGroups(groups, savedRollout.getTargetFilterQuery(), savedRollout.getCreatedAt())
.getTargetsPerGroup().forEach(this::assertTargetsPerRolloutGroupQuota);
validateTargetsInGroups(groups, savedRollout.getTargetFilterQuery(), savedRollout.getCreatedAt(),
distributionSetType.getId()).getTargetsPerGroup().forEach(this::assertTargetsPerRolloutGroupQuota);
}
// create and persist the groups (w/o filling them with targets)
@@ -299,21 +312,6 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
new RolloutGroupCreatedEvent(group, rollout.getId(), eventPublisherHolder.getApplicationId())));
}
@Override
@Async
public ListenableFuture<RolloutGroupsValidation> validateTargetsInGroups(final List<RolloutGroupCreate> groups,
final String targetFilter, final Long createdAt) {
final String baseFilter = RolloutHelper.getTargetFilterQuery(targetFilter, createdAt);
final long totalTargets = targetManagement.countByRsql(baseFilter);
if (totalTargets == 0) {
throw new ConstraintDeclarationException("Rollout target filter does not match any targets");
}
return new AsyncResult<>(validateTargetsInGroups(
groups.stream().map(RolloutGroupCreate::build).collect(Collectors.toList()), baseFilter, totalTargets));
}
@Override
@Transactional
@Retryable(include = {
@@ -524,7 +522,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
@Override
public Page<Rollout> findAllWithDetailedStatus(final Pageable pageable, final boolean deleted) {
Page<JpaRollout> rollouts;
final Page<JpaRollout> rollouts;
final Specification<JpaRollout> spec = RolloutSpecification.isDeletedWithDistributionSet(deleted);
rollouts = rolloutRepository.findAll(spec, pageable);
setRolloutStatusDetails(rollouts);
@@ -599,8 +597,6 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
* Enforces the quota defining the maximum number of {@link Target}s per
* {@link RolloutGroup}.
*
* @param group
* The rollout group
* @param requested
* number of targets to check
*/
@@ -626,4 +622,93 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
});
}
private RolloutGroupsValidation validateTargetsInGroups(final List<RolloutGroup> groups, final String baseFilter,
final long totalTargets, final Long dsTypeId) {
final List<Long> groupTargetCounts = new ArrayList<>(groups.size());
final Map<String, Long> targetFilterCounts = groups.stream()
.map(group -> RolloutHelper.getGroupTargetFilter(baseFilter, group)).distinct()
.collect(Collectors.toMap(Function.identity(),
groupTargetFilter -> targetManagement.countByRsqlAndCompatible(groupTargetFilter, dsTypeId)));
long unusedTargetsCount = 0;
for (int i = 0; i < groups.size(); i++) {
final RolloutGroup group = groups.get(i);
final String groupTargetFilter = RolloutHelper.getGroupTargetFilter(baseFilter, group);
RolloutHelper.verifyRolloutGroupTargetPercentage(group.getTargetPercentage());
final long targetsInGroupFilter = targetFilterCounts.get(groupTargetFilter);
final long overlappingTargets = countOverlappingTargetsWithPreviousGroups(baseFilter, groups, group, i,
targetFilterCounts);
final long realTargetsInGroup;
// Assume that targets which were not used in the previous groups
// are used in this group
if (overlappingTargets > 0 && unusedTargetsCount > 0) {
realTargetsInGroup = targetsInGroupFilter - overlappingTargets + unusedTargetsCount;
unusedTargetsCount = 0;
} else {
realTargetsInGroup = targetsInGroupFilter - overlappingTargets;
}
final long reducedTargetsInGroup = Math
.round(group.getTargetPercentage() / 100 * (double) realTargetsInGroup);
groupTargetCounts.add(reducedTargetsInGroup);
unusedTargetsCount += realTargetsInGroup - reducedTargetsInGroup;
}
return new RolloutGroupsValidation(totalTargets, groupTargetCounts);
}
private long countOverlappingTargetsWithPreviousGroups(final String baseFilter, final List<RolloutGroup> groups,
final RolloutGroup group, final int groupIndex, final Map<String, Long> targetFilterCounts) {
// there can't be overlapping targets in the first group
if (groupIndex == 0) {
return 0;
}
final List<RolloutGroup> previousGroups = groups.subList(0, groupIndex);
final String overlappingTargetsFilter = RolloutHelper.getOverlappingWithGroupsTargetFilter(baseFilter,
previousGroups, group);
if (targetFilterCounts.containsKey(overlappingTargetsFilter)) {
return targetFilterCounts.get(overlappingTargetsFilter);
} else {
final long overlappingTargets = targetManagement.countByRsql(overlappingTargetsFilter);
targetFilterCounts.put(overlappingTargetsFilter, overlappingTargets);
return overlappingTargets;
}
}
private long calculateRemainingTargets(final List<RolloutGroup> groups, final String targetFilter,
final Long createdAt, final Long dsTypeId) {
final String baseFilter = RolloutHelper.getTargetFilterQuery(targetFilter, createdAt);
final long totalTargets = targetManagement.countByRsqlAndCompatible(baseFilter, dsTypeId);
if (totalTargets == 0) {
throw new ConstraintDeclarationException("Rollout target filter does not match any targets");
}
final RolloutGroupsValidation validation = validateTargetsInGroups(groups, baseFilter, totalTargets, dsTypeId);
return totalTargets - validation.getTargetsInGroups();
}
@Override
@Async
public ListenableFuture<RolloutGroupsValidation> validateTargetsInGroups(final List<RolloutGroupCreate> groups,
final String targetFilter, final Long createdAt, final Long dsTypeId) {
final String baseFilter = RolloutHelper.getTargetFilterQuery(targetFilter, createdAt);
final long totalTargets = targetManagement.countByRsqlAndCompatible(baseFilter, dsTypeId);
if (totalTargets == 0) {
throw new ConstraintDeclarationException("Rollout target filter does not match any targets");
}
return new AsyncResult<>(
validateTargetsInGroups(groups.stream().map(RolloutGroupCreate::build).collect(Collectors.toList()),
baseFilter, totalTargets, dsTypeId));
}
}

View File

@@ -786,10 +786,14 @@ public class JpaTargetManagement implements TargetManagement {
public long countByRsql(final String targetFilterQuery) {
final Specification<JpaTarget> specs = RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class,
virtualPropertyReplacer, database);
return targetRepository.count((root, query, cb) -> {
query.distinct(true);
return specs.toPredicate(root, query, cb);
});
return targetRepository.count(specs);
}
@Override
public long countByRsqlAndCompatible(final String targetFilterQuery, final Long dsTypeId) {
final Specification<JpaTarget> rsqlSpec = RSQLUtility.buildRsqlSpecification(targetFilterQuery,
TargetFields.class, virtualPropertyReplacer, database);
return targetRepository.count(rsqlSpec.and(TargetSpecifications.isCompatibleWithDistributionSetType(dsTypeId)));
}
@Override

View File

@@ -664,17 +664,15 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
@Bean
@ConditionalOnMissingBean
RolloutManagement rolloutManagement(final TargetManagement targetManagement,
final DeploymentManagement deploymentManagement, final RolloutGroupManagement rolloutGroupManagement,
final DistributionSetManagement distributionSetManagement, final ApplicationContext context,
final EventPublisherHolder eventPublisherHolder, final VirtualPropertyReplacer virtualPropertyReplacer,
final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry,
final JpaProperties properties, final RolloutApprovalStrategy rolloutApprovalStrategy,
final DistributionSetManagement distributionSetManagement, final EventPublisherHolder eventPublisherHolder,
final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager,
final TenantAware tenantAware, final LockRegistry lockRegistry, final JpaProperties properties,
final RolloutApprovalStrategy rolloutApprovalStrategy,
final TenantConfigurationManagement tenantConfigurationManagement,
final SystemSecurityContext systemSecurityContext, final RolloutExecutor rolloutExecutor) {
return new JpaRolloutManagement(targetManagement, deploymentManagement, rolloutGroupManagement,
distributionSetManagement, context, eventPublisherHolder, virtualPropertyReplacer, txManager,
tenantAware, lockRegistry, properties.getDatabase(), rolloutApprovalStrategy,
tenantConfigurationManagement, systemSecurityContext, rolloutExecutor);
return new JpaRolloutManagement(targetManagement, distributionSetManagement, eventPublisherHolder,
virtualPropertyReplacer, txManager, tenantAware, lockRegistry, properties.getDatabase(),
rolloutApprovalStrategy, tenantConfigurationManagement, systemSecurityContext, rolloutExecutor);
}
/**

View File

@@ -394,7 +394,7 @@ public final class TargetSpecifications {
// isNull predicate first
final Predicate targetTypeIsNull = targetRoot.get(JpaTarget_.targetType).isNull();
return cb.or(targetTypeIsNull, getDistSetTypeEqualPredicate(targetRoot, cb, distributionSetTypeId));
return cb.or(targetTypeIsNull, cb.equal(getDsTypeIdPath(targetRoot), distributionSetTypeId));
};
}
@@ -414,19 +414,21 @@ public final class TargetSpecifications {
// isNotNull predicate first
final Predicate targetTypeNotNull = targetRoot.get(JpaTarget_.targetType).isNotNull();
// We need to check for isNull(...) and notEqual(...) since we allow
// target types that don't have any compatible distribution set type
return cb.and(targetTypeNotNull,
cb.isNull(getDistSetTypeEqualPredicate(targetRoot, cb, distributionSetTypeId)));
cb.or(cb.isNull(getDsTypeIdPath(targetRoot)),
cb.notEqual(getDsTypeIdPath(targetRoot), distributionSetTypeId)));
};
}
private static Predicate getDistSetTypeEqualPredicate(final Root<JpaTarget> root, final CriteriaBuilder cb,
final Long dsTypeId) {
private static Path<Long> getDsTypeIdPath(final Root<JpaTarget> root) {
final Join<JpaTarget, JpaTargetType> targetTypeJoin = root.join(JpaTarget_.targetType, JoinType.LEFT);
targetTypeJoin.fetch(JpaTargetType_.distributionSetTypes);
final SetJoin<JpaTargetType, JpaDistributionSetType> dsTypeTargetTypeJoin = targetTypeJoin
.join(JpaTargetType_.distributionSetTypes, JoinType.LEFT);
return cb.equal(dsTypeTargetTypeJoin.get(JpaDistributionSetType_.id), dsTypeId);
return dsTypeTargetTypeJoin.get(JpaDistributionSetType_.id);
}
/**

View File

@@ -41,9 +41,9 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TenantConfigurationCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TenantConfigurationUpdatedEvent;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.exception.DistributionSetTypeNotInTargetTypeException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.ForceQuitActionNotAllowedException;
import org.eclipse.hawkbit.repository.exception.IncompatibleTargetTypeException;
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException;
import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException;
@@ -62,6 +62,7 @@ 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.DistributionSetType;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetType;
@@ -1442,14 +1443,31 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
@Description("Verify that the DistributionSet assignment fails for target with incompatible target type.")
void verifyDSAssignmentFailsForTargetsWithIncompatibleTargetTypes() {
final DistributionSet ds = testdataFactory.createDistributionSet("test-ds");
final TargetType targetType = testdataFactory.createTargetType("test-type", Collections.emptyList());
final DistributionSetType dsType = testdataFactory.findOrCreateDistributionSetType("test-ds-type", "dsType");
final TargetType targetType = testdataFactory.createTargetType("target-type",
Collections.singletonList(dsType));
final Target target = testdataFactory.createTarget("test-target", "test-target", targetType.getId());
final DeploymentRequest deploymentRequest = DeploymentManagement
.deploymentRequest(target.getControllerId(), ds.getId()).build();
final List<DeploymentRequest> deploymentRequests = Collections.singletonList(deploymentRequest);
assertThatExceptionOfType(DistributionSetTypeNotInTargetTypeException.class)
assertThatExceptionOfType(IncompatibleTargetTypeException.class)
.isThrownBy(() -> deploymentManagement.assignDistributionSets(deploymentRequests));
}
@Test
@Description("Verify that the DistributionSet assignment fails for target with target type that is not compatible with any dsType.")
void verifyDSAssignmentFailsForTargetsWithTargetTypesThatAreNotCompatibleWithAnyDs() {
final DistributionSet ds = testdataFactory.createDistributionSet("test-ds");
final TargetType emptyTargetType = testdataFactory.createTargetType("target-type", Collections.emptyList());
final Target targetWithEmptyType = testdataFactory.createTarget("test-target", "test-target",
emptyTargetType.getId());
final DeploymentRequest deploymentRequestWithEmptyType = DeploymentManagement
.deploymentRequest(targetWithEmptyType.getControllerId(), ds.getId()).build();
final List<DeploymentRequest> deploymentRequests = Collections.singletonList(deploymentRequestWithEmptyType);
assertThatExceptionOfType(IncompatibleTargetTypeException.class)
.isThrownBy(() -> deploymentManagement.assignDistributionSets(deploymentRequests));
}

View File

@@ -1205,8 +1205,7 @@ public class TestdataFactory {
/**
* Finds {@link TargetType} in repository with given
* {@link TargetType#getName()} or creates if it does not exist yet. No ds
* types
* {@link TargetType#getName()} or creates if it does not exist yet. No ds types
* are assigned on creation.
*
* @param targetTypeName
@@ -1222,8 +1221,7 @@ public class TestdataFactory {
/**
* Creates {@link TargetType} in repository with given
* {@link TargetType#getName()}. Compatible distribution set types are
* assigned
* {@link TargetType#getName()}. Compatible distribution set types are assigned
* on creation
*
* @param targetTypeName
@@ -1256,8 +1254,8 @@ public class TestdataFactory {
}
/**
* Creates a distribution set and directly invalidates it. No actions will
* be canceled and no rollouts will be stopped with this invalidation.
* Creates a distribution set and directly invalidates it. No actions will be
* canceled and no rollouts will be stopped with this invalidation.
*
* @return created invalidated {@link DistributionSet}
*/
@@ -1269,8 +1267,8 @@ public class TestdataFactory {
}
/**
* Creates a distribution set that has no software modules assigned, so it
* is incomplete.
* Creates a distribution set that has no software modules assigned, so it is
* incomplete.
*
* @return created incomplete {@link DistributionSet}
*/