Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ public static class Dos {
* Maximum number of allowed groups per Rollout.
*/
private int maxRolloutGroupsPerRollout = 500;
/**
* Maximum number of allowed distinct target groups.
*/
private int maxTargetGroups = 100;
/**
* Maximum number of messages per ActionStatus
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ public interface QuotaManagement {
*/
int getMaxTargetsPerRolloutGroup();

/**
* @return the maximum number of distinct target groups
*/
int getMaxTargetGroups();

/**
* @return the maximum number of target distribution set assignments resulting from a manual assignment
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ public int getMaxTargetsPerRolloutGroup() {
return securityProperties.getDos().getMaxTargetsPerRolloutGroup();
}

@Override
public int getMaxTargetGroups() {
return securityProperties.getDos().getMaxTargetGroups();
}

@Override
public int getMaxTargetDistributionSetAssignmentsPerManualAssignment() {
return securityProperties.getDos().getMaxTargetDistributionSetAssignmentsPerManualAssignment();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
Expand All @@ -34,6 +36,7 @@
import jakarta.validation.constraints.NotEmpty;

import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.context.AccessContext;
import org.eclipse.hawkbit.ql.jpa.QLSupport;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.TargetManagement;
Expand Down Expand Up @@ -102,6 +105,50 @@ protected JpaTargetManagement(
this.targetTagRepository = targetTagRepository;
}

@Override
@Transactional
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public JpaTarget create(final TargetManagement.Create create) {
assertTargetGroupQuota(Collections.singletonList(create.getGroup()));
Comment thread
strailov marked this conversation as resolved.
return super.create(create);
}

@Override
@Transactional
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public List<JpaTarget> create(final Collection<TargetManagement.Create> create) {
assertTargetGroupQuota(create.stream().map(TargetManagement.Create::getGroup).toList());
return super.create(create);
}

@Override
@Transactional
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public JpaTarget update(final TargetManagement.Update update) {
try {
assertTargetGroupQuota(Collections.singletonList(update.getGroup()));
} catch (final Exception ex) {
// target existence check in order to throw EntityNotFound instead of AssignmentQuotaException if both applicable
getValid(update.getId());
throw ex;
}
return super.update(update);
}

@Override
@Transactional
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public Map<Long, JpaTarget> update(final Collection<TargetManagement.Update> update) {
try {
assertTargetGroupQuota(update.stream().map(TargetManagement.Update::getGroup).toList());
} catch (final Exception ex) {
// target existence check in order to throw EntityNotFound instead of AssignmentQuotaException if both applicable
get(update.stream().map(TargetManagement.Update::getId).toList());
throw ex;
}
return super.update(update);
}

@Override
public Map<String, String> getControllerAttributes(final String controllerId) {
return getMap(controllerId, JpaTarget_.controllerAttributes);
Expand Down Expand Up @@ -339,6 +386,8 @@ public Target unassignType(final String controllerId) {
@Transactional
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public void assignTargetGroupWithRsql(String group, String rsql) {
// Quota check
assertTargetGroupQuota(Collections.singletonList(group));

// Switch back to UpdateAllQuery if switching back to hibernate. (EclipseLink does not work well with UpdateAllQuery)
// EclipseLink: using subquery approach — applying predicate directly to the UPDATE root
Expand Down Expand Up @@ -401,6 +450,9 @@ private void assignTargetGroupOnChunks(final String group, final String rsql) {
@Transactional
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public void assignTargetsWithGroup(String group, List<String> controllerIds) {
// Quota check
assertTargetGroupQuota(Collections.singletonList(group));

final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaUpdate<JpaTarget> criteriaQuery = cb.createCriteriaUpdate(JpaTarget.class);
Root<JpaTarget> root = criteriaQuery.from(JpaTarget.class);
Expand Down Expand Up @@ -552,4 +604,34 @@ private void throwEntityNotFoundExceptionIfTagDoesNotExist(final Long tagId) {
throw new EntityNotFoundException(TargetTag.class, tagId);
}
}

private void assertTargetGroupQuota(final Collection<String> requested) {
final SortedSet<String> wanted = requested.stream()
.filter(Objects::nonNull)
.collect(Collectors.toCollection(TreeSet::new));

if (wanted.isEmpty()) {
return; // no group(s), skip findDistinctGroups db call
}

final long limit = quotaManagement.getMaxTargetGroups();
if (limit <= 0) {
return;
}

// one group, already present -> allowed
if (wanted.size() == 1 && jpaRepository.existsByGroup(wanted.first())) {
return;
}
final List<String> existing = jpaRepository.findDistinctGroups(AccessContext.tenant());
Comment thread
strailov marked this conversation as resolved.

existing.forEach(wanted::remove);
if (wanted.isEmpty()) {
return; // no growth -> allowed
}

QuotaHelper.assertAssignmentQuota(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use method with parrentId (when no such) instead of:
public static void assertAssignmentQuota(
final long requested, final long limit,
@NotNull final Class type, @NotNull final Class parentType) {
i.e. parent null?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we won't get into :
if (parentId != null && countFct != null) in the assertAssignmentQuota where we want to evaluate currentCount + requested > limit . Didn't want to modify the method just for one parameter, which just needs to be non null (and why not the parent to be the Tenant in the exception message).

AccessContext.tenant(), wanted.size(), limit, "target group", "tenant",
tenant -> existing.size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,12 @@ void setAssignedAndInstalledDistributionSetAndUpdateStatus(
*/
@Query(value = "SELECT DISTINCT target_group FROM sp_target WHERE tenant = ?1 AND target_group IS NOT NULL", nativeQuery = true)
List<String> findDistinctGroups(@Param("tenant") String tenant);

/**
* Checks if a target group is present
*
* @param group to check existence
* @return whether a target group is present or not
*/
boolean existsByGroup(String group);
}
Loading
Loading