Feature soft autoassignment (#789)

* Repository layer and database migration
* Changed target filter management to update auto assignment action type together with distribution set, extended mgmt API, adapted auto assign checker/scheduler, changed auto assign distribution set select table to combobox, added filter functionality (needs refactoring)
* Refactored auto assignment dialog window, added soft/forced option group, restructured action type option group layout
* Added forced icon to target filter table autoassignment cell for the forced auto assign actions
* First working draft of distribution set select combobox for auto assignment window
* Optimised filtering queries, added alphabetical sorting on distribution set name and version
* Refactoring of distribution bean query for lazy loading
* Distribution set combobox refactoring and comments
* Added verification of auto assign distribution set validity (completed, not deleted), exdended target filter query fields with auto assign action type field, added rsql filter tests, added repository layer tests
* Added mgmt API tests
* Changed target filter rest docu tests to include auto-assignment type
* Updated hawkbit docs with auto-assignment description
* Added debouncing to key and value input fields of metadata popup layout to get rid of unnecessary value change events, removed redundant set value call in common dialog window, minimizing the repaint components calls
* Fixed sonar findings
* Reverted changes of common dialog window validaton, setting the value of the field explicitly as before, until we rethink the whole concept of validaton mechanism
* Fixed review findings
* Removed rsql filtering by filter auto-assign action type, due to missing conversion of disallowed timeforced action type
* Small fix regarding usage of page request
* Updated sql script version for flyway
* Extended tests for soft deleted distribution sets for auto-assignment and filter string within distribution set specification builder

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>
This commit is contained in:
Bondar Bogdan
2019-03-13 13:35:22 +01:00
committed by Stefan Behl
parent 185f88e19b
commit 664c467920
63 changed files with 1891 additions and 750 deletions

View File

@@ -267,7 +267,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement {
if (!assigned.isEmpty()) {
final Long[] dsIds = assigned.toArray(new Long[assigned.size()]);
distributionSetRepository.deleteDistributionSet(dsIds);
targetFilterQueryRepository.unsetAutoAssignDistributionSet(dsIds);
targetFilterQueryRepository.unsetAutoAssignDistributionSetAndActionType(dsIds);
}
// mark the rest as hard delete
@@ -276,6 +276,8 @@ public class JpaDistributionSetManagement implements DistributionSetManagement {
// hard delete the rest if exists
if (!toHardDelete.isEmpty()) {
targetFilterQueryRepository
.unsetAutoAssignDistributionSetAndActionType(toHardDelete.toArray(new Long[toHardDelete.size()]));
// don't give the delete statement an empty list, JPA/Oracle cannot
// handle the empty list
distributionSetRepository.deleteByIdIn(toHardDelete);
@@ -602,7 +604,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement {
private static List<Specification<JpaDistributionSet>> buildDistributionSetSpecifications(
final DistributionSetFilter distributionSetFilter) {
final List<Specification<JpaDistributionSet>> specList = Lists.newArrayListWithExpectedSize(7);
final List<Specification<JpaDistributionSet>> specList = Lists.newArrayListWithExpectedSize(8);
Specification<JpaDistributionSet> spec;
@@ -626,6 +628,14 @@ public class JpaDistributionSetManagement implements DistributionSetManagement {
specList.add(spec);
}
if (!StringUtils.isEmpty(distributionSetFilter.getFilterString())) {
final String[] dsFilterNameAndVersionEntries = getDsFilterNameAndVersionEntries(
distributionSetFilter.getFilterString().trim());
spec = DistributionSetSpecification.likeNameAndVersion(dsFilterNameAndVersionEntries[0],
dsFilterNameAndVersionEntries[1]);
specList.add(spec);
}
if (isDSWithNoTagSelected(distributionSetFilter) || isTagsSelected(distributionSetFilter)) {
spec = DistributionSetSpecification.hasTags(distributionSetFilter.getTagNames(),
distributionSetFilter.getSelectDSWithNoTag());
@@ -642,6 +652,19 @@ public class JpaDistributionSetManagement implements DistributionSetManagement {
return specList;
}
// the format of filter string is 'name:version'. 'name' and 'version'
// fields follow the starts_with semantic, that changes to equal for 'name'
// field when the semicolon is present
private static String[] getDsFilterNameAndVersionEntries(final String filterString) {
final int semicolonIndex = filterString.indexOf(':');
final String dsFilterName = semicolonIndex != -1 ? filterString.substring(0, semicolonIndex)
: (filterString + "%");
final String dsFilterVersion = semicolonIndex != -1 ? (filterString.substring(semicolonIndex + 1) + "%") : "%";
return new String[] { !StringUtils.isEmpty(dsFilterName) ? dsFilterName : "%", dsFilterVersion };
}
private void assertDistributionSetIsNotAssignedToTargets(final Long distributionSet) {
if (actionRepository.countByDistributionSetId(distributionSet) > 0) {
throw new EntityReadOnlyException(String.format(

View File

@@ -22,6 +22,8 @@ import org.eclipse.hawkbit.repository.builder.GenericTargetFilterQueryUpdate;
import org.eclipse.hawkbit.repository.builder.TargetFilterQueryCreate;
import org.eclipse.hawkbit.repository.builder.TargetFilterQueryUpdate;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException;
import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignDistributionSetException;
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryCreate;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
@@ -30,6 +32,7 @@ import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetFilterQuerySpecification;
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
@@ -88,7 +91,9 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
// enforce the 'max targets per auto assign' quota right here even if
// the result of the filter query can vary over time
create.getSet().flatMap(set -> create.getQuery()).ifPresent(this::assertMaxTargetsQuota);
if (create.getAutoAssignDistributionSetId().isPresent()) {
create.getQuery().ifPresent(this::assertMaxTargetsQuota);
}
return targetFilterQueryRepository.save(create.build());
}
@@ -217,24 +222,53 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
@Override
@Transactional
public TargetFilterQuery updateAutoAssignDS(final long queryId, final Long dsId) {
public TargetFilterQuery updateAutoAssignDSWithActionType(final long queryId, final Long dsId,
final ActionType actionType) {
final JpaTargetFilterQuery targetFilterQuery = findTargetFilterQueryOrThrowExceptionIfNotFound(queryId);
targetFilterQuery.setAutoAssignDistributionSet(
Optional.ofNullable(dsId).map(this::findDistributionSetAndThrowExceptionIfNotFound).orElse(null));
// we cannot be sure that the quota was enforced at creation time
// because the Target Filter Query REST API does not allow to specify an
// auto-assign distribution set when creating a target filter query
if (dsId != null) {
if (dsId == null) {
targetFilterQuery.setAutoAssignDistributionSet(null);
targetFilterQuery.setAutoAssignActionType(null);
} else {
// we cannot be sure that the quota was enforced at creation time
// because the Target Filter Query REST API does not allow to
// specify an
// auto-assign distribution set when creating a target filter query
assertMaxTargetsQuota(targetFilterQuery.getQuery());
final JpaDistributionSet distributionSetToAutoAssign = findDistributionSetAndThrowExceptionIfNotFound(dsId);
// must be completed and not soft deleted
verifyDistributionSetAndThrowExceptionIfNotValid(distributionSetToAutoAssign);
targetFilterQuery.setAutoAssignDistributionSet(distributionSetToAutoAssign);
// the action type is set to FORCED per default (when not explicitly
// specified)
targetFilterQuery.setAutoAssignActionType(sanitizeAutoAssignActionType(actionType));
}
return targetFilterQueryRepository.save(targetFilterQuery);
}
private static void verifyDistributionSetAndThrowExceptionIfNotValid(final DistributionSet distributionSet) {
if (!distributionSet.isComplete() || distributionSet.isDeleted()) {
throw new InvalidAutoAssignDistributionSetException();
}
}
private static ActionType sanitizeAutoAssignActionType(final ActionType actionType) {
if (actionType == null) {
return ActionType.FORCED;
}
if (!TargetFilterQuery.ALLOWED_AUTO_ASSIGN_ACTION_TYPES.contains(actionType)) {
throw new InvalidAutoAssignActionTypeException();
}
return actionType;
}
private JpaDistributionSet findDistributionSetAndThrowExceptionIfNotFound(final Long setId) {
return (JpaDistributionSet) distributionSetManagement.getWithDetails(setId)
return (JpaDistributionSet) distributionSetManagement.get(setId)
.orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, setId));
}

View File

@@ -45,15 +45,16 @@ public interface TargetFilterQueryRepository
Page<JpaTargetFilterQuery> findAll();
/**
* Sets the auto assign distribution sets to null which match the ds ids.
* Sets the auto assign distribution sets and action types to null which
* match the ds ids.
*
* @param dsIds
* distribution set ids to be set to null
*/
@Modifying
@Transactional
@Query("update JpaTargetFilterQuery d set d.autoAssignDistributionSet = NULL where d.autoAssignDistributionSet in :ids")
void unsetAutoAssignDistributionSet(@Param("ids") Long... dsIds);
@Query("update JpaTargetFilterQuery d set d.autoAssignDistributionSet = NULL, d.autoAssignActionType = NULL where d.autoAssignDistributionSet in :ids")
void unsetAutoAssignDistributionSetAndActionType(@Param("ids") Long... dsIds);
/**
* Deletes all {@link TenantAwareBaseEntity} of a given tenant. For safety

View File

@@ -17,7 +17,8 @@ import org.eclipse.hawkbit.exception.AbstractServerRtException;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.RepositoryModelConstants;
import org.eclipse.hawkbit.repository.model.Target;
@@ -28,12 +29,9 @@ import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Checks if targets need a new distribution set (DS) based on the target filter
@@ -52,7 +50,7 @@ public class AutoAssignChecker {
private final DeploymentManagement deploymentManagement;
private final TransactionTemplate transactionTemplate;
private final PlatformTransactionManager transactionManager;
/**
* Maximum for target filter queries with auto assign DS Maximum for targets
@@ -84,13 +82,7 @@ public class AutoAssignChecker {
this.targetFilterQueryManagement = targetFilterQueryManagement;
this.targetManagement = targetManagement;
this.deploymentManagement = deploymentManagement;
final DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName("autoAssignDSToTargets");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
def.setReadOnly(false);
def.setIsolationLevel(Isolation.READ_COMMITTED.value());
transactionTemplate = new TransactionTemplate(transactionManager, def);
this.transactionManager = transactionManager;
}
/**
@@ -104,8 +96,7 @@ public class AutoAssignChecker {
final PageRequest pageRequest = PageRequest.of(0, PAGE_SIZE);
final Page<TargetFilterQuery> filterQueries = targetFilterQueryManagement
.findWithAutoAssignDS(pageRequest);
final Page<TargetFilterQuery> filterQueries = targetFilterQueryManagement.findWithAutoAssignDS(pageRequest);
for (final TargetFilterQuery filterQuery : filterQueries) {
checkByTargetFilterQueryAndAssignDS(filterQuery);
@@ -149,15 +140,17 @@ public class AutoAssignChecker {
*/
private int runTransactionalAssignment(final TargetFilterQuery targetFilterQuery, final Long dsId) {
final String actionMessage = String.format(ACTION_MESSAGE, targetFilterQuery.getName());
return transactionTemplate.execute(status -> {
final List<TargetWithActionType> targets = getTargetsWithActionType(targetFilterQuery.getQuery(), dsId,
PAGE_SIZE);
final int count = targets.size();
if (count > 0) {
deploymentManagement.assignDistributionSet(dsId, targets, actionMessage);
}
return count;
});
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();
if (count > 0) {
deploymentManagement.assignDistributionSet(dsId, targets, actionMessage);
}
return count;
});
}
/**
@@ -169,17 +162,22 @@ public class AutoAssignChecker {
* @param dsId
* dsId the targets are not allowed to have in their action
* history
* @param type
* action type for targets auto assignment
* @param count
* maximum amount of targets to retrieve
* @return list of targets with action type
*/
private List<TargetWithActionType> getTargetsWithActionType(final String targetFilterQuery, final Long dsId,
final int count) {
final Page<Target> targets = targetManagement
.findByTargetFilterQueryAndNonDS(PageRequest.of(0, count), dsId, targetFilterQuery);
final ActionType type, final int count) {
final Page<Target> targets = targetManagement.findByTargetFilterQueryAndNonDS(PageRequest.of(0, count), dsId,
targetFilterQuery);
// the action type is set to FORCED per default (when not explicitly
// specified)
final ActionType autoAssignActionType = type == null ? ActionType.FORCED : type;
return targets.getContent().stream().map(t -> new TargetWithActionType(t.getControllerId(),
Action.ActionType.FORCED, RepositoryModelConstants.NO_FORCE_TIME)).collect(Collectors.toList());
autoAssignActionType, RepositoryModelConstants.NO_FORCE_TIME)).collect(Collectors.toList());
}
}

View File

@@ -12,8 +12,11 @@ import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.builder.AbstractTargetFilterQueryUpdateCreate;
import org.eclipse.hawkbit.repository.builder.TargetFilterQueryCreate;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetFilterQuery;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
/**
* Create/build implementation.
@@ -32,7 +35,8 @@ public class JpaTargetFilterQueryCreate extends AbstractTargetFilterQueryUpdateC
public JpaTargetFilterQuery build() {
return new JpaTargetFilterQuery(name, query,
getSet().map(this::findDistributionSetAndThrowExceptionIfNotFound).orElse(null));
getAutoAssignDistributionSetId().map(this::findDistributionSetAndThrowExceptionIfNotFound).orElse(null),
getAutoAssignActionType().filter(JpaTargetFilterQueryCreate::isAutoAssignActionTypeValid).orElse(null));
}
private DistributionSet findDistributionSetAndThrowExceptionIfNotFound(final Long setId) {
@@ -40,4 +44,12 @@ public class JpaTargetFilterQueryCreate extends AbstractTargetFilterQueryUpdateC
.orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, setId));
}
private static boolean isAutoAssignActionTypeValid(final ActionType actionType) {
if (!TargetFilterQuery.ALLOWED_AUTO_ASSIGN_ACTION_TYPES.contains(actionType)) {
throw new InvalidAutoAssignActionTypeException();
}
return true;
}
}

View File

@@ -23,10 +23,15 @@ import javax.validation.constraints.Size;
import org.eclipse.hawkbit.repository.event.remote.TargetFilterQueryDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetFilterQueryCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetFilterQueryUpdatedEvent;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
import org.eclipse.persistence.annotations.ConversionValue;
import org.eclipse.persistence.annotations.Convert;
import org.eclipse.persistence.annotations.ObjectTypeConverter;
import org.eclipse.persistence.descriptors.DescriptorEvent;
/**
@@ -41,7 +46,7 @@ import org.eclipse.persistence.descriptors.DescriptorEvent;
@SuppressWarnings("squid:S2160")
public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
implements TargetFilterQuery, EventAwareEntity {
private static final long serialVersionUID = 7493966984413479089L;
private static final long serialVersionUID = 1L;
@Column(name = "name", length = NamedEntity.NAME_MAX_SIZE, nullable = false)
@Size(max = NamedEntity.NAME_MAX_SIZE)
@@ -57,6 +62,13 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
@JoinColumn(name = "auto_assign_distribution_set", nullable = true, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_filter_auto_assign_ds"))
private JpaDistributionSet autoAssignDistributionSet;
@Column(name = "auto_assign_action_type", nullable = true)
@ObjectTypeConverter(name = "autoAssignActionType", objectType = Action.ActionType.class, dataType = Integer.class, conversionValues = {
@ConversionValue(objectValue = "FORCED", dataValue = "0"),
@ConversionValue(objectValue = "SOFT", dataValue = "1") })
@Convert("autoAssignActionType")
private ActionType autoAssignActionType;
public JpaTargetFilterQuery() {
// Default constructor for JPA.
}
@@ -70,12 +82,15 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
* of the {@link TargetFilterQuery}.
* @param autoAssignDistributionSet
* of the {@link TargetFilterQuery}.
* @param autoAssignActionType
* of the {@link TargetFilterQuery}.
*/
public JpaTargetFilterQuery(final String name, final String query,
final DistributionSet autoAssignDistributionSet) {
public JpaTargetFilterQuery(final String name, final String query, final DistributionSet autoAssignDistributionSet,
final ActionType autoAssignActionType) {
this.name = name;
this.query = query;
this.autoAssignDistributionSet = (JpaDistributionSet) autoAssignDistributionSet;
this.autoAssignActionType = autoAssignActionType;
}
@Override
@@ -105,6 +120,15 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
this.autoAssignDistributionSet = distributionSet;
}
@Override
public ActionType getAutoAssignActionType() {
return autoAssignActionType;
}
public void setAutoAssignActionType(final ActionType actionType) {
this.autoAssignActionType = actionType;
}
@Override
public void fireCreateEvent(final DescriptorEvent descriptorEvent) {
EventPublisherHolder.getInstance().getEventPublisher().publishEvent(

View File

@@ -120,6 +120,22 @@ public final class DistributionSetSpecification {
cb.like(cb.lower(targetRoot.<String> get(JpaDistributionSet_.description)), subString.toLowerCase()));
}
/**
* {@link Specification} for retrieving {@link DistributionSet}s by "like
* name and like version".
*
* @param name
* to be filtered on
* @param version
* to be filtered on
* @return the {@link DistributionSet} {@link Specification}
*/
public static Specification<JpaDistributionSet> likeNameAndVersion(final String name, final String version) {
return (targetRoot, query, cb) -> cb.and(
cb.like(cb.lower(targetRoot.<String> get(JpaDistributionSet_.name)), name.toLowerCase()),
cb.like(cb.lower(targetRoot.<String> get(JpaDistributionSet_.version)), version.toLowerCase()));
}
/**
* {@link Specification} for retrieving {@link DistributionSet}s by "has at
* least one of the given tag names".

View File

@@ -22,6 +22,7 @@ import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
@@ -83,10 +84,30 @@ public final class DeploymentHelper {
*/
public static <T> T runInNewTransaction(@NotNull final PlatformTransactionManager txManager,
final String transactionName, @NotNull final TransactionCallback<T> action) {
return runInNewTransaction(txManager, transactionName, Isolation.DEFAULT.value(), action);
}
/**
* Executes the modifying action in new transaction
*
* @param txManager
* transaction manager interface
* @param transactionName
* the name of the new transaction
* @param isolationLevel
* isolation level of the new transaction
* @param action
* the callback to execute in new tranaction
*
* @return the result of the action
*/
public static <T> T runInNewTransaction(@NotNull final PlatformTransactionManager txManager,
final String transactionName, final int isolationLevel, @NotNull final TransactionCallback<T> action) {
final DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName(transactionName);
def.setReadOnly(false);
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
def.setIsolationLevel(isolationLevel);
return new TransactionTemplate(txManager, def).execute(action);
}
}

View File

@@ -0,0 +1 @@
ALTER TABLE sp_target_filter_query ADD COLUMN auto_assign_action_type INTEGER;

View File

@@ -0,0 +1 @@
ALTER TABLE sp_target_filter_query ADD COLUMN auto_assign_action_type integer;

View File

@@ -0,0 +1 @@
ALTER TABLE sp_target_filter_query ADD COLUMN auto_assign_action_type integer;

View File

@@ -0,0 +1 @@
ALTER TABLE sp_target_filter_query ADD auto_assign_action_type INTEGER;