From 19caff6e46bd3efa5a3a651283d880ffbb95473b Mon Sep 17 00:00:00 2001 From: Stefan Behl Date: Thu, 19 Jul 2018 15:23:14 +0200 Subject: [PATCH] Feature action cleanup (#704) * Initial commit * Tenant configuration enhancements * Update REST documentation * Enhance System Configuration view in UI * Add unit tests * Fix delete query for H2 * Improve test coverage * Fix Sonar findings * Fix Hawkbit bot findings * Fix PR review findings * Fix peer review findings Signed-off-by: Stefan Behl --- .../repository/DeploymentManagement.java | 16 + .../TenantConfigurationProperties.java | 17 + .../TenantConfigurationLongValidator.java | 21 ++ .../hawkbit-repository-defaults.properties | 14 + .../repository/jpa/ActionRepository.java | 14 +- .../jpa/ActionStatusRepository.java | 2 +- .../jpa/DistributionSetRepository.java | 2 +- .../jpa/DistributionSetTagRepository.java | 4 +- .../jpa/JpaDeploymentManagement.java | 49 ++- .../RepositoryApplicationConfiguration.java | 50 ++- .../jpa/RolloutGroupRepository.java | 4 +- .../jpa/RolloutTargetGroupRepository.java | 2 +- .../jpa/SoftwareModuleRepository.java | 2 +- .../jpa/TargetFilterQueryRepository.java | 2 +- .../repository/jpa/TargetRepository.java | 20 +- .../jpa/autocleanup/AutoActionCleanup.java | 111 ++++++ .../jpa/autocleanup/AutoCleanupScheduler.java | 97 ++++++ .../jpa/autocleanup/CleanupTask.java | 27 ++ .../autocleanup/AutoActionCleanupTest.java | 227 +++++++++++++ .../autocleanup/AutoCleanupSchedulerTest.java | 84 +++++ .../TenantResourceDocumentationTest.java | 10 +- .../AuthenticationConfigurationView.java | 19 +- .../DefaultDistributionSetTypeLayout.java | 17 +- .../RepositoryConfigurationView.java | 34 +- .../RolloutConfigurationView.java | 46 +-- .../ActionAutocleanupConfigurationItem.java | 321 ++++++++++++++++++ .../ui/utils/UIComponentIdProvider.java | 16 + .../src/main/resources/messages.properties | 20 +- 28 files changed, 1165 insertions(+), 83 deletions(-) create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationLongValidator.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanup.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoCleanupScheduler.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autocleanup/CleanupTask.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanupTest.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoCleanupSchedulerTest.java create mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/repository/ActionAutocleanupConfigurationItem.java diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java index ee2a961fd..f4e6f23ff 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.repository; import java.util.Collection; import java.util.Optional; +import java.util.Set; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; @@ -457,4 +458,19 @@ public interface DeploymentManagement { * if target with given ID does not exist */ Optional getInstalledDistributionSet(@NotEmpty String controllerId); + + /** + * Deletes actions which match one of the given action status and which have + * not been modified since the given (absolute) time-stamp. + * + * @param status + * Set of action status. + * @param lastModified + * A time-stamp in milliseconds. + * + * @return The number of action entries that were deleted. + */ + @PreAuthorize(SpringEvalExpressions.IS_SYSTEM_CODE) + int deleteActionsByStatusAndLastModifiedBefore(@NotNull Set status, long lastModified); + } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java index 626fda3db..dab824d7b 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java @@ -61,6 +61,7 @@ public class TenantConfigurationProperties { * */ public static class TenantConfigurationKey { + /** * Header based authentication enabled. */ @@ -70,6 +71,7 @@ public class TenantConfigurationProperties { * Header based authentication authority name. */ public static final String AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME = "authentication.header.authority"; + /** * Target token based authentication enabled. */ @@ -125,6 +127,21 @@ public class TenantConfigurationProperties { */ public static final String REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED = "repository.actions.autoclose.enabled"; + /** + * Switch to enable/disable automatic action cleanup. + */ + public static final String ACTION_CLEANUP_ENABLED = "action.cleanup.enabled"; + + /** + * Specifies the action expiry in milli-seconds. + */ + public static final String ACTION_CLEANUP_ACTION_EXPIRY = "action.cleanup.actionExpiry"; + + /** + * Specifies the action status. + */ + public static final String ACTION_CLEANUP_ACTION_STATUS = "action.cleanup.actionStatus"; + private String keyName; private String defaultValue = ""; private Class dataType = String.class; diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationLongValidator.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationLongValidator.java new file mode 100644 index 000000000..d298e98d7 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationLongValidator.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2018 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.tenancy.configuration.validator; + +/** + * Specific tenant configuration validator, which validates that the given value + * is a {@link Long}. + */ +public class TenantConfigurationLongValidator implements TenantConfigurationValidator { + + @Override + public Class validateToClass() { + return Long.class; + } +} diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties b/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties index 553501886..e363bbd85 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties +++ b/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties @@ -77,4 +77,18 @@ hawkbit.server.tenant.configuration.rollout-approval-enabled.defaultValue=false hawkbit.server.tenant.configuration.rollout-approval-enabled.dataType=java.lang.Boolean hawkbit.server.tenant.configuration.rollout-approval-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator +hawkbit.server.tenant.configuration.action-cleanup-enabled.keyName=action.cleanup.enabled +hawkbit.server.tenant.configuration.action-cleanup-enabled.defaultValue=false +hawkbit.server.tenant.configuration.action-cleanup-enabled.dataType=java.lang.Boolean +hawkbit.server.tenant.configuration.action-cleanup-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator + +hawkbit.server.tenant.configuration.action-cleanup-action-expiry.keyName=action.cleanup.actionExpiry +# default: 30 days +hawkbit.server.tenant.configuration.action-cleanup-action-expiry.defaultValue=2592000000 +hawkbit.server.tenant.configuration.action-cleanup-action-expiry.dataType=java.lang.Long +hawkbit.server.tenant.configuration.action-cleanup-action-expiry.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationLongValidator + +hawkbit.server.tenant.configuration.action-cleanup-action-status.keyName=action.cleanup.actionStatus +hawkbit.server.tenant.configuration.action-cleanup-action-status.defaultValue=CANCELED,ERROR + # Default tenant configuration - END diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java index 36fd11eae..1da3b9dd4 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java @@ -63,7 +63,7 @@ public interface ActionRepository extends BaseEntityRepository, * the {@link DistributionSet} on which will be filtered * @return the found {@link Action}s */ - Page findByDistributionSetId(final Pageable pageable, final Long dsId); + Page findByDistributionSetId(Pageable pageable, Long dsId); /** * Retrieves all {@link Action}s which are referring the given @@ -98,7 +98,7 @@ public interface ActionRepository extends BaseEntityRepository, * the action active flag * @return the found {@link Action}s */ - List findByTargetAndActiveOrderByIdAsc(final JpaTarget target, boolean active); + List findByTargetAndActiveOrderByIdAsc(JpaTarget target, boolean active); /** * Retrieves the oldest {@link Action} that is active and referring to the @@ -114,7 +114,7 @@ public interface ActionRepository extends BaseEntityRepository, * @return the found {@link Action} */ @EntityGraph(value = "Action.ds", type = EntityGraphType.LOAD) - Optional findFirstByTargetControllerIdAndActive(final Sort sort, final String controllerId, boolean active); + Optional findFirstByTargetControllerIdAndActive(Sort sort, String controllerId, boolean active); /** * Checks if an active action exists for given @@ -139,8 +139,7 @@ public interface ActionRepository extends BaseEntityRepository, * assigned {@link DistributionSet}. */ @Query("Select a from JpaAction a join a.distributionSet ds join ds.modules modul where a.target.controllerId = :target and modul.id = :module order by a.id desc") - List findActionByTargetAndSoftwareModule(@Param("target") final String targetId, - @Param("module") Long moduleId); + List findActionByTargetAndSoftwareModule(@Param("target") String targetId, @Param("module") Long moduleId); /** * Retrieves all {@link Action}s which are referring the given @@ -155,7 +154,7 @@ public interface ActionRepository extends BaseEntityRepository, * @return the found {@link Action}s */ @Query("Select a from JpaAction a where a.target = :target and a.distributionSet = :ds") - Page findByTargetAndDistributionSet(final Pageable pageable, @Param("target") final JpaTarget target, + Page findByTargetAndDistributionSet(Pageable pageable, @Param("target") JpaTarget target, @Param("ds") JpaDistributionSet ds); /** @@ -453,5 +452,6 @@ public interface ActionRepository extends BaseEntityRepository, @Transactional // Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477 @Query("DELETE FROM JpaAction a WHERE a.id IN ?1") - void deleteByIdIn(final Collection actionIDs); + void deleteByIdIn(Collection actionIDs); + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionStatusRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionStatusRepository.java index b25d65472..0de073d7b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionStatusRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionStatusRepository.java @@ -90,6 +90,6 @@ public interface ActionStatusRepository * @return Page with found status messages. */ @Query("SELECT message FROM JpaActionStatus actionstatus JOIN actionstatus.messages message WHERE actionstatus.action.id = :actionId AND message NOT LIKE :filter") - Page findMessagesByActionIdAndMessageNotLike(final Pageable pageable, @Param("actionId") Long actionId, + Page findMessagesByActionIdAndMessageNotLike(Pageable pageable, @Param("actionId") Long actionId, @Param("filter") String filter); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetRepository.java index b15bc5b48..f8edc6c8f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetRepository.java @@ -48,7 +48,7 @@ public interface DistributionSetRepository * @return list of found {@link DistributionSet}s */ @Query(value = "Select Distinct ds from JpaDistributionSet ds join ds.tags dst where dst.id = :tag and ds.deleted = 0") - Page findByTag(Pageable pageable, @Param("tag") final Long tagId); + Page findByTag(Pageable pageable, @Param("tag") Long tagId); /** * deletes the {@link DistributionSet}s with the given IDs. diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetTagRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetTagRepository.java index f7f85dfe5..2c319bfa6 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetTagRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetTagRepository.java @@ -40,7 +40,7 @@ public interface DistributionSetTagRepository */ @Modifying @Transactional - Long deleteByName(final String tagName); + Long deleteByName(String tagName); /** * find {@link DistributionSetTag} by its name. @@ -49,7 +49,7 @@ public interface DistributionSetTagRepository * to filter on * @return the {@link DistributionSetTag} if found, otherwise null */ - Optional findByNameEquals(final String tagName); + Optional findByNameEquals(String tagName); /** * Checks if tag with given name exists. diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java index 80daba275..d126b1970 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java @@ -18,8 +18,10 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.IntStream; import javax.persistence.EntityManager; +import javax.persistence.Query; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.JoinType; @@ -61,6 +63,7 @@ import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.eclipse.hawkbit.security.SystemSecurityContext; +import org.eclipse.hawkbit.tenancy.TenantAware; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -86,6 +89,7 @@ import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.CollectionUtils; import org.springframework.validation.annotation.Validated; +import com.google.common.base.Joiner; import com.google.common.collect.Lists; /** @@ -102,6 +106,8 @@ public class JpaDeploymentManagement implements DeploymentManagement { */ private static final int ACTION_PAGE_LIMIT = 1000; + private static final String QUERY_DELETE_ACTIONS_BY_STATE_AND_LAST_MODIFIED = "DELETE FROM sp_action WHERE tenant=#tenant AND status IN (%s) AND last_modified_at<#last_modified_at LIMIT 1000"; + private final EntityManager entityManager; private final ActionRepository actionRepository; private final DistributionSetRepository distributionSetRepository; @@ -119,6 +125,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { private final TenantConfigurationManagement tenantConfigurationManagement; private final QuotaManagement quotaManagement; private final SystemSecurityContext systemSecurityContext; + private final TenantAware tenantAware; private final Database database; JpaDeploymentManagement(final EntityManager entityManager, final ActionRepository actionRepository, @@ -128,7 +135,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { final ApplicationContext applicationContext, final AfterTransactionCommitExecutor afterCommit, final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager, final TenantConfigurationManagement tenantConfigurationManagement, final QuotaManagement quotaManagement, - final SystemSecurityContext systemSecurityContext, final Database database) { + final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware, final Database database) { this.entityManager = entityManager; this.actionRepository = actionRepository; this.distributionSetRepository = distributionSetRepository; @@ -148,6 +155,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { this.tenantConfigurationManagement = tenantConfigurationManagement; this.quotaManagement = quotaManagement; this.systemSecurityContext = systemSecurityContext; + this.tenantAware = tenantAware; this.database = database; } @@ -388,7 +396,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { throw new ForceQuitActionNotAllowedException(action.getId() + " is not active and cannot be force quit"); } - LOG.warn("action ({}) was still activ and has been force quite.", action); + LOG.warn("action ({}) was still active and has been force quite.", action); // document that the status has been retrieved actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELED, System.currentTimeMillis(), @@ -689,4 +697,41 @@ public class JpaDeploymentManagement implements DeploymentManagement { return distributionSetRepository.findInstalledAtTarget(controllerId); } + @Override + @Transactional(readOnly = false) + public int deleteActionsByStatusAndLastModifiedBefore(final Set status, final long lastModified) { + if (status.isEmpty()) { + return 0; + } + /* + * We use a native query here because Spring JPA does not support to + * specify a LIMIT clause on a DELETE statement. However, for this + * specific use case (action cleanup), we must specify a row limit to + * reduce the overall load on the database. + */ + + final int statusCount = status.size(); + final Status[] statusArr = status.toArray(new Status[statusCount]); + + final String queryStr = String.format(QUERY_DELETE_ACTIONS_BY_STATE_AND_LAST_MODIFIED, + formatInClauseWithNumberKeys(statusCount)); + final Query deleteQuery = entityManager.createNativeQuery(queryStr); + + IntStream.range(0, statusCount) + .forEach(i -> deleteQuery.setParameter(String.valueOf(i), statusArr[i].ordinal())); + deleteQuery.setParameter("tenant", tenantAware.getCurrentTenant().toUpperCase()); + deleteQuery.setParameter("last_modified_at", lastModified); + + LOG.debug("Action cleanup: Executing the following (native) query: {}", deleteQuery); + return deleteQuery.executeUpdate(); + } + + private static String formatInClauseWithNumberKeys(final int count) { + return formatInClause(IntStream.range(0, count).mapToObj(String::valueOf).collect(Collectors.toList())); + } + + private static String formatInClause(final Collection elements) { + return "#" + Joiner.on(",#").join(elements); + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index b4e4d221b..5ee71d9a4 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.repository.jpa; +import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; @@ -51,6 +52,9 @@ import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent; import org.eclipse.hawkbit.repository.jpa.aspects.ExceptionMappingAspectHandler; import org.eclipse.hawkbit.repository.jpa.autoassign.AutoAssignChecker; import org.eclipse.hawkbit.repository.jpa.autoassign.AutoAssignScheduler; +import org.eclipse.hawkbit.repository.jpa.autocleanup.AutoActionCleanup; +import org.eclipse.hawkbit.repository.jpa.autocleanup.AutoCleanupScheduler; +import org.eclipse.hawkbit.repository.jpa.autocleanup.CleanupTask; import org.eclipse.hawkbit.repository.jpa.builder.JpaDistributionSetBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaDistributionSetTypeBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaRolloutBuilder; @@ -567,7 +571,6 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { lockRegistry, properties.getDatabase(), rolloutApprovalStrategy); } - /** * {@link DefaultRolloutApprovalStrategy} bean. * @@ -613,11 +616,12 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final AfterTransactionCommitExecutor afterCommit, final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager, final TenantConfigurationManagement tenantConfigurationManagement, final QuotaManagement quotaManagement, - final SystemSecurityContext systemSecurityContext, final JpaProperties properties) { + final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware, + final JpaProperties properties) { return new JpaDeploymentManagement(entityManager, actionRepository, distributionSetRepository, targetRepository, actionStatusRepository, targetManagement, auditorProvider, eventPublisher, applicationContext, afterCommit, virtualPropertyReplacer, txManager, tenantConfigurationManagement, quotaManagement, - systemSecurityContext, properties.getDatabase()); + systemSecurityContext, tenantAware, properties.getDatabase()); } /** @@ -730,6 +734,46 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { return new AutoAssignScheduler(systemManagement, systemSecurityContext, autoAssignChecker, lockRegistry); } + /** + * {@link AutoActionCleanup} bean. + * + * @param deploymentManagement + * Deployment management service + * @param configManagement + * Tenant configuration service + * + * @return a new {@link AutoActionCleanup} bean + */ + @Bean + CleanupTask actionCleanup(final DeploymentManagement deploymentManagement, + final TenantConfigurationManagement configManagement) { + return new AutoActionCleanup(deploymentManagement, configManagement); + } + + /** + * {@link AutoCleanupScheduler} bean. + * + * @param systemManagement + * to find all tenants + * @param systemSecurityContext + * to run as system + * @param lockRegistry + * to lock the tenant for auto assignment + * @param cleanupTasks + * a list of cleanup tasks + * + * @return a new {@link AutoCleanupScheduler} bean + */ + @Bean + @ConditionalOnMissingBean + @Profile("!test") + @ConditionalOnProperty(prefix = "hawkbit.autocleanup.scheduler", name = "enabled", matchIfMissing = true) + AutoCleanupScheduler autoCleanupScheduler(final SystemManagement systemManagement, + final SystemSecurityContext systemSecurityContext, final LockRegistry lockRegistry, + final List cleanupTasks) { + return new AutoCleanupScheduler(systemManagement, systemSecurityContext, lockRegistry, cleanupTasks); + } + /** * {@link RolloutScheduler} bean. * diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutGroupRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutGroupRepository.java index fd8b4cd08..1555eb0f2 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutGroupRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutGroupRepository.java @@ -144,7 +144,7 @@ public interface RolloutGroupRepository * the page request to sort, limit the result * @return a page of found {@link RolloutGroup} or {@code empty}. */ - Page findByRolloutId(final Long rolloutId, Pageable page); + Page findByRolloutId(Long rolloutId, Pageable page); /** * Counts all {@link RolloutGroup} for a specific rollout. @@ -154,7 +154,7 @@ public interface RolloutGroupRepository * * @return the amount of found {@link RolloutGroup}s. */ - long countByRolloutId(final Long rolloutId); + long countByRolloutId(Long rolloutId); @Modifying @Query("DELETE FROM JpaRolloutGroup g where g.id in :rolloutGroupIds") diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutTargetGroupRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutTargetGroupRepository.java index 475f4f89a..72a86e4ed 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutTargetGroupRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutTargetGroupRepository.java @@ -30,5 +30,5 @@ public interface RolloutTargetGroupRepository * the group to filter for * @return count of targets in the group */ - Long countByRolloutGroup(final JpaRolloutGroup rolloutGroup); + Long countByRolloutGroup(JpaRolloutGroup rolloutGroup); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleRepository.java index 290e1b107..9c4a46e75 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleRepository.java @@ -75,7 +75,7 @@ public interface SoftwareModuleRepository @Transactional @Query("UPDATE JpaSoftwareModule b SET b.deleted = 1, b.lastModifiedAt = :lastModifiedAt, b.lastModifiedBy = :lastModifiedBy WHERE b.id IN :ids") void deleteSoftwareModule(@Param("lastModifiedAt") Long modifiedAt, @Param("lastModifiedBy") String modifiedBy, - @Param("ids") final Long... ids); + @Param("ids") Long... ids); /** * @param pageable diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryRepository.java index 9b8d07fee..ab05474ad 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryRepository.java @@ -36,7 +36,7 @@ public interface TargetFilterQueryRepository * @param name * @return custom target filter */ - Optional findByName(final String name); + Optional findByName(String name); /** * Find list of all custom target filters. diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java index 23a0a8877..556e47c73 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java @@ -112,7 +112,7 @@ public interface TargetRepository extends BaseEntityRepository, @Transactional // Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477 @Query("DELETE FROM JpaTarget t WHERE t.id IN ?1") - void deleteByIdIn(final Collection targetIDs); + void deleteByIdIn(Collection targetIDs); /** * Finds {@link Target}s by assigned {@link Tag}. @@ -125,7 +125,7 @@ public interface TargetRepository extends BaseEntityRepository, * @return page of found targets */ @Query(value = "SELECT DISTINCT t FROM JpaTarget t JOIN t.tags tt WHERE tt.id = :tag") - Page findByTag(Pageable page, @Param("tag") final Long tagId); + Page findByTag(Pageable page, @Param("tag") Long tagId); /** * Finds all {@link Target}s based on given {@link Target#getControllerId()} @@ -138,8 +138,8 @@ public interface TargetRepository extends BaseEntityRepository, * @return {@link List} of found {@link Target}s. */ @Query(value = "SELECT DISTINCT t from JpaTarget t JOIN t.tags tt WHERE tt.name = :tagname AND t.controllerId IN :targets") - List findByTagNameAndControllerIdIn(@Param("tagname") final String tag, - @Param("targets") final Collection controllerIds); + List findByTagNameAndControllerIdIn(@Param("tagname") String tag, + @Param("targets") Collection controllerIds); /** * Used by UI to filter based on selected status. @@ -151,7 +151,7 @@ public interface TargetRepository extends BaseEntityRepository, * * @return found targets */ - Page findByUpdateStatus(final Pageable pageable, final TargetUpdateStatus status); + Page findByUpdateStatus(Pageable pageable, TargetUpdateStatus status); /** * retrieves the {@link Target}s which has the {@link DistributionSet} @@ -163,7 +163,7 @@ public interface TargetRepository extends BaseEntityRepository, * the ID of the {@link DistributionSet} * @return the found {@link Target}s */ - Page findByInstalledDistributionSetId(final Pageable pageable, final Long setID); + Page findByInstalledDistributionSetId(Pageable pageable, Long setID); /** * Finds all targets that have defined {@link DistributionSet} assigned. @@ -175,7 +175,7 @@ public interface TargetRepository extends BaseEntityRepository, * * @return page of found targets */ - Page findByAssignedDistributionSetId(final Pageable pageable, final Long setID); + Page findByAssignedDistributionSetId(Pageable pageable, Long setID); /** * Counts number of targets with given distribution set Id. @@ -185,7 +185,7 @@ public interface TargetRepository extends BaseEntityRepository, * * @return number of found {@link Target}s. */ - Long countByAssignedDistributionSetId(final Long distId); + Long countByAssignedDistributionSetId(Long distId); /** * Counts number of targets with given distribution set Id. @@ -194,7 +194,7 @@ public interface TargetRepository extends BaseEntityRepository, * to search for * @return number of found {@link Target}s. */ - Long countByInstalledDistributionSetId(final Long distId); + Long countByInstalledDistributionSetId(Long distId); /** * Finds all {@link Target}s in the repository. @@ -221,7 +221,7 @@ public interface TargetRepository extends BaseEntityRepository, * the page request parameter * @return a page of all targets related to a rollout group */ - Page findByRolloutTargetGroupRolloutGroupId(final Long rolloutGroupId, Pageable page); + Page findByRolloutTargetGroupRolloutGroupId(Long rolloutGroupId, Pageable page); /** * Finds all targets related to a target rollout group stored for a specific diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanup.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanup.java new file mode 100644 index 000000000..d4d278a1b --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanup.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2018 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.jpa.autocleanup; + +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.ACTION_CLEANUP_ACTION_EXPIRY; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.ACTION_CLEANUP_ACTION_STATUS; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.ACTION_CLEANUP_ENABLED; + +import java.io.Serializable; +import java.time.Instant; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.TenantConfigurationValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A cleanup task for {@link Action} entities which can be used to delete + * actions which are in a certain {@link Action.Status}. It is recommended to + * only clean up actions which have terminated already (i.e. actions in status + * CANCELLED or ERROR). + * + * The cleanup task can be enabled /disabled and configured on a per tenant + * basis. + */ +public class AutoActionCleanup implements CleanupTask { + + private static final Logger LOGGER = LoggerFactory.getLogger(AutoActionCleanup.class); + + private static final String ID = "action-cleanup"; + private static final boolean ACTION_CLEANUP_ENABLED_DEFAULT = false; + private static final long ACTION_CLEANUP_ACTION_EXPIRY_DEFAULT = TimeUnit.DAYS.toMillis(30); + private static final EnumSet EMPTY_STATUS_SET = EnumSet.noneOf(Status.class); + + private final DeploymentManagement deploymentMgmt; + private final TenantConfigurationManagement config; + + /** + * Constructs the action cleanup handler. + * + * @param deploymentMgmt + * The {@link DeploymentManagement} to operate on. + * @param configMgmt + * The {@link TenantConfigurationManagement} service. + */ + public AutoActionCleanup(final DeploymentManagement deploymentMgmt, + final TenantConfigurationManagement configMgmt) { + this.deploymentMgmt = deploymentMgmt; + this.config = configMgmt; + } + + @Override + public void run() { + + if (!isEnabled()) { + LOGGER.debug("Action cleanup is disabled for this tenant..."); + return; + } + + final EnumSet status = getActionStatus(); + if (!status.isEmpty()) { + final long lastModified = System.currentTimeMillis() - getExpiry(); + final int actionsCount = deploymentMgmt.deleteActionsByStatusAndLastModifiedBefore(status, lastModified); + LOGGER.debug("Deleted {} actions in status {} which have not been modified since {} ({})", actionsCount, + status, Instant.ofEpochMilli(lastModified), lastModified); + } + } + + @Override + public String getId() { + return ID; + } + + private long getExpiry() { + final TenantConfigurationValue expiry = getConfigValue(ACTION_CLEANUP_ACTION_EXPIRY, Long.class); + return expiry != null ? expiry.getValue() : ACTION_CLEANUP_ACTION_EXPIRY_DEFAULT; + } + + private EnumSet getActionStatus() { + final TenantConfigurationValue statusStr = getConfigValue(ACTION_CLEANUP_ACTION_STATUS, String.class); + if (statusStr != null) { + return Arrays.stream(statusStr.getValue().split("[;,]")).map(Status::valueOf) + .collect(Collectors.toCollection(() -> EnumSet.noneOf(Status.class))); + } + return EMPTY_STATUS_SET; + } + + private boolean isEnabled() { + final TenantConfigurationValue isEnabled = getConfigValue(ACTION_CLEANUP_ENABLED, Boolean.class); + return isEnabled != null ? isEnabled.getValue() : ACTION_CLEANUP_ENABLED_DEFAULT; + } + + private TenantConfigurationValue getConfigValue(final String key, + final Class valueType) { + return config.getConfigurationValue(key, valueType); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoCleanupScheduler.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoCleanupScheduler.java new file mode 100644 index 000000000..60b19f3eb --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoCleanupScheduler.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2018 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.jpa.autocleanup; + +import java.util.List; +import java.util.concurrent.locks.Lock; + +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.security.SystemSecurityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.integration.support.locks.LockRegistry; +import org.springframework.scheduling.annotation.Scheduled; + +/** + * A scheduler to invoke a set of cleanup handlers periodically. + */ +public class AutoCleanupScheduler { + + private static final Logger LOGGER = LoggerFactory.getLogger(AutoCleanupScheduler.class); + + private static final String AUTO_CLEANUP = "auto-cleanup"; + private static final String SEP = "."; + private static final String PROP_AUTO_CLEANUP_INTERVAL = "${hawkbit.autocleanup.scheduler.fixedDelay:86400000}"; + + private final SystemManagement systemManagement; + private final SystemSecurityContext systemSecurityContext; + private final LockRegistry lockRegistry; + private final List cleanupTasks; + + /** + * Constructs the cleanup schedulers and initializes it with a set of + * cleanup handlers. + * + * @param systemManagement + * Management APIs to invoke actions in a certain tenant context. + * @param systemSecurityContext + * The system security context. + * @param lockRegistry + * A registry for shared locks. + * @param cleanupTasks + * A list of cleanup tasks. + */ + public AutoCleanupScheduler(final SystemManagement systemManagement, + final SystemSecurityContext systemSecurityContext, final LockRegistry lockRegistry, + final List cleanupTasks) { + this.systemManagement = systemManagement; + this.systemSecurityContext = systemSecurityContext; + this.lockRegistry = lockRegistry; + this.cleanupTasks = cleanupTasks; + } + + /** + * Scheduler method which kicks off the cleanup process. + */ + @Scheduled(initialDelayString = PROP_AUTO_CLEANUP_INTERVAL, fixedDelayString = PROP_AUTO_CLEANUP_INTERVAL) + public void run() { + LOGGER.debug("Auto cleanup scheduler has been triggered."); + // run this code in system code privileged to have the necessary + // permission to query and create entities + if (!cleanupTasks.isEmpty()) { + systemSecurityContext.runAsSystem(this::executeAutoCleanup); + } + } + + /** + * Method which executes each registered cleanup task for each tenant. + */ + @SuppressWarnings("squid:S3516") + private Void executeAutoCleanup() { + systemManagement.forEachTenant(tenant -> cleanupTasks.forEach(task -> { + final Lock lock = obtainLock(task, tenant); + if (!lock.tryLock()) { + return; + } + try { + task.run(); + } catch (final RuntimeException e) { + LOGGER.error("Cleanup task failed.", e); + } finally { + lock.unlock(); + } + })); + return null; + } + + private Lock obtainLock(final CleanupTask task, final String tenant) { + return lockRegistry.obtain(AUTO_CLEANUP + SEP + task.getId() + SEP + tenant); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autocleanup/CleanupTask.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autocleanup/CleanupTask.java new file mode 100644 index 000000000..405c4152d --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autocleanup/CleanupTask.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2018 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.jpa.autocleanup; + +/** + * Interface modeling a cleanup task. + */ +public interface CleanupTask extends Runnable { + + /** + * Executes the cleanup task. + */ + @Override + void run(); + + /** + * @return The identifier of this cleanup task. Never null. + */ + String getId(); + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanupTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanupTest.java new file mode 100644 index 000000000..e546700ae --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoActionCleanupTest.java @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2018 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.jpa.autocleanup; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.ACTION_CLEANUP_ACTION_EXPIRY; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.ACTION_CLEANUP_ACTION_STATUS; +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; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * Test class for {@link AutoActionCleanup}. + * + */ +@Features("Component Tests - Repository") +@Stories("Action cleanup handler") +public class AutoActionCleanupTest extends AbstractJpaIntegrationTest { + + @Autowired + private AutoActionCleanup autoActionCleanup; + + @Test + @Description("Verifies that running actions are not cleaned up.") + public void runningActionsAreNotCleanedUp() { + + // cleanup config for this test case + setupCleanupConfiguration(true, 0, Action.Status.CANCELED, Action.Status.ERROR); + + final Target trg1 = testdataFactory.createTarget("trg1"); + final Target trg2 = testdataFactory.createTarget("trg2"); + + 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())); + + assertThat(actionRepository.count()).isEqualTo(2); + + autoActionCleanup.run(); + + assertThat(actionRepository.count()).isEqualTo(2); + + } + + @Test + @Description("Verifies that nothing is cleaned up if the cleanup is disabled.") + public void cleanupDisabled() { + + // cleanup config for this test case + setupCleanupConfiguration(false, 0, Action.Status.CANCELED); + + final Target trg1 = testdataFactory.createTarget("trg1"); + final Target trg2 = testdataFactory.createTarget("trg2"); + + final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); + final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); + + final Long action1 = deploymentManagement.assignDistributionSet(ds1.getId(), ActionType.FORCED, 0, + Collections.singletonList(trg1.getControllerId())).getActions().get(0); + deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, + Collections.singletonList(trg2.getControllerId())); + + setActionToCanceled(action1); + + assertThat(actionRepository.count()).isEqualTo(2); + + autoActionCleanup.run(); + + assertThat(actionRepository.count()).isEqualTo(2); + + } + + @Test + @Description("Verifies that canceled and failed actions are cleaned up.") + public void canceledAndFailedActionsAreCleanedUp() { + + // cleanup config for this test case + setupCleanupConfiguration(true, 0, Action.Status.CANCELED, Action.Status.ERROR); + + final Target trg1 = testdataFactory.createTarget("trg1"); + final Target trg2 = testdataFactory.createTarget("trg2"); + final Target trg3 = testdataFactory.createTarget("trg3"); + + final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); + final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); + + final Long action1 = deploymentManagement.assignDistributionSet(ds1.getId(), ActionType.FORCED, 0, + Collections.singletonList(trg1.getControllerId())).getActions().get(0); + final Long action2 = deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, + Collections.singletonList(trg2.getControllerId())).getActions().get(0); + final Long action3 = deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, + Collections.singletonList(trg3.getControllerId())).getActions().get(0); + + assertThat(actionRepository.count()).isEqualTo(3); + + setActionToCanceled(action1); + setActionToFailed(action2); + + assertThat(actionRepository.count()).isEqualTo(3); + + autoActionCleanup.run(); + + assertThat(actionRepository.count()).isEqualTo(1); + assertThat(actionRepository.getById(action3)).isPresent(); + + } + + @Test + @Description("Verifies that canceled actions are cleaned up.") + public void canceledActionsAreCleanedUp() { + + // cleanup config for this test case + setupCleanupConfiguration(true, 0, Action.Status.CANCELED); + + final Target trg1 = testdataFactory.createTarget("trg1"); + final Target trg2 = testdataFactory.createTarget("trg2"); + final Target trg3 = testdataFactory.createTarget("trg3"); + + final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); + final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); + + final Long action1 = deploymentManagement.assignDistributionSet(ds1.getId(), ActionType.FORCED, 0, + Collections.singletonList(trg1.getControllerId())).getActions().get(0); + final Long action2 = deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, + Collections.singletonList(trg2.getControllerId())).getActions().get(0); + final Long action3 = deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, + Collections.singletonList(trg3.getControllerId())).getActions().get(0); + + assertThat(actionRepository.count()).isEqualTo(3); + + setActionToCanceled(action1); + setActionToFailed(action2); + + assertThat(actionRepository.count()).isEqualTo(3); + + autoActionCleanup.run(); + + assertThat(actionRepository.count()).isEqualTo(2); + assertThat(actionRepository.getById(action2)).isPresent(); + assertThat(actionRepository.getById(action3)).isPresent(); + + } + + @Test + @Description("Verifies that canceled and failed actions are cleaned up once they expired.") + @SuppressWarnings("squid:S2925") + public void canceledAndFailedActionsAreCleanedUpWhenExpired() throws InterruptedException { + + // cleanup config for this test case + setupCleanupConfiguration(true, 500, Action.Status.CANCELED, Action.Status.ERROR); + + final Target trg1 = testdataFactory.createTarget("trg1"); + final Target trg2 = testdataFactory.createTarget("trg2"); + final Target trg3 = testdataFactory.createTarget("trg3"); + + final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); + final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); + + final Long action1 = deploymentManagement.assignDistributionSet(ds1.getId(), ActionType.FORCED, 0, + Collections.singletonList(trg1.getControllerId())).getActions().get(0); + final Long action2 = deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, + Collections.singletonList(trg2.getControllerId())).getActions().get(0); + final Long action3 = deploymentManagement.assignDistributionSet(ds2.getId(), ActionType.FORCED, 0, + Collections.singletonList(trg3.getControllerId())).getActions().get(0); + + assertThat(actionRepository.count()).isEqualTo(3); + + setActionToCanceled(action1); + setActionToFailed(action2); + + autoActionCleanup.run(); + + // actions have not expired yet + assertThat(actionRepository.count()).isEqualTo(3); + + // wait for expiry to elapse + Thread.sleep(800); + + autoActionCleanup.run(); + + assertThat(actionRepository.count()).isEqualTo(1); + assertThat(actionRepository.getById(action3)).isPresent(); + + } + + private void setActionToCanceled(final Long id) { + deploymentManagement.cancelAction(id); + deploymentManagement.forceQuitAction(id); + } + + private void setActionToFailed(final Long id) { + controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(id).status(Status.ERROR)); + } + + private void setupCleanupConfiguration(final boolean cleanupEnabled, final long expiry, final Status... status) { + tenantConfigurationManagement.addOrUpdateConfiguration(ACTION_CLEANUP_ENABLED, cleanupEnabled); + tenantConfigurationManagement.addOrUpdateConfiguration(ACTION_CLEANUP_ACTION_EXPIRY, expiry); + tenantConfigurationManagement.addOrUpdateConfiguration(ACTION_CLEANUP_ACTION_STATUS, + Arrays.stream(status).map(Status::toString).collect(Collectors.joining(","))); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoCleanupSchedulerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoCleanupSchedulerTest.java new file mode 100644 index 000000000..712086a2d --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autocleanup/AutoCleanupSchedulerTest.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2018 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.jpa.autocleanup; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.integration.support.locks.LockRegistry; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * Test class for {@link AutoCleanupScheduler}. + * + */ +@Features("Component Tests - Repository") +@Stories("Auto cleanup scheduler") +public class AutoCleanupSchedulerTest extends AbstractJpaIntegrationTest { + + private final AtomicInteger counter = new AtomicInteger(); + + @Autowired + private LockRegistry lockRegistry; + + @Before + public void setUp() { + counter.set(0); + } + + @Test + @Description("Verifies that all cleanup handlers are executed regardless if one of them throws an error") + public void executeHandlerChain() { + + new AutoCleanupScheduler(systemManagement, systemSecurityContext, lockRegistry, Arrays.asList( + new SuccessfulCleanup(), new SuccessfulCleanup(), new FailingCleanup(), new SuccessfulCleanup())).run(); + + assertThat(counter.get()).isEqualTo(4); + + } + + private class SuccessfulCleanup implements CleanupTask { + + @Override + public void run() { + counter.incrementAndGet(); + } + + @Override + public String getId() { + return "success"; + } + + } + + private class FailingCleanup implements CleanupTask { + + @Override + public void run() { + counter.incrementAndGet(); + throw new RuntimeException("cleanup failed"); + } + + @Override + public String getId() { + return "success"; + } + + } + +} diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java index d0f019b21..409fcd26f 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java @@ -67,9 +67,9 @@ public class TenantResourceDocumentationTest extends AbstractApiRestDocumentatio CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, "the period of time after the SP server will recognize a target, which is not performing pull requests anymore."); CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.POLLING_TIME_INTERVAL, - "the time intervall between two poll requests of a target."); + "the time interval between two poll requests of a target."); CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.MIN_POLLING_TIME_INTERVAL, - "the smallest time intervallpermittet between two poll requests of a target."); + "the smallest time interval permitted between two poll requests of a target."); CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.MAINTENANCE_WINDOW_POLL_COUNT, "the polling interval so that controller tries to poll at least these many times between the last " + "polling and before start of maintenance window. The polling interval is" @@ -82,6 +82,12 @@ public class TenantResourceDocumentationTest extends AbstractApiRestDocumentatio "if autoclose running actions with new Distribution Set assignment is enabled."); CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, "if approval mode for Rollout Management is enabled."); + CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.ACTION_CLEANUP_ENABLED, + "if automatic cleanup of deployment actions is enabled."); + CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.ACTION_CLEANUP_ACTION_STATUS, + "the list of action status that should be taken into account for the cleanup."); + CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.ACTION_CLEANUP_ACTION_EXPIRY, + "the expiry time in milliseconds that needs to elapse before an action may be cleaned up."); } @Autowired diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/AuthenticationConfigurationView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/AuthenticationConfigurationView.java index d0d8e8340..519788a3e 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/AuthenticationConfigurationView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/AuthenticationConfigurationView.java @@ -22,6 +22,7 @@ import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; import com.vaadin.data.Property.ValueChangeEvent; import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.ui.Alignment; import com.vaadin.ui.CheckBox; import com.vaadin.ui.GridLayout; import com.vaadin.ui.Label; @@ -87,17 +88,14 @@ public class AuthenticationConfigurationView extends BaseConfigurationView vLayout.setMargin(true); vLayout.setSizeFull(); - final Label headerDisSetType = new Label(i18n.getMessage("configuration.authentication.title")); - headerDisSetType.addStyleName("config-panel-header"); - vLayout.addComponent(headerDisSetType); + final Label header = new Label(i18n.getMessage("configuration.authentication.title")); + header.addStyleName("config-panel-header"); + vLayout.addComponent(header); - final Link linkToSecurityHelp = SPUIComponentProvider - .getHelpLink(uiProperties.getLinks().getDocumentation().getSecurity()); - vLayout.addComponent(linkToSecurityHelp); - - final GridLayout gridLayout = new GridLayout(2, 4); + final GridLayout gridLayout = new GridLayout(3, 4); gridLayout.setSpacing(true); gridLayout.setImmediate(true); + gridLayout.setSizeFull(); gridLayout.setColumnExpandRatio(1, 1.0F); certificateAuthCheckbox = SPUIComponentProvider.getCheckBox("", DIST_CHECKBOX_STYLE, null, false, ""); @@ -130,6 +128,11 @@ public class AuthenticationConfigurationView extends BaseConfigurationView gridLayout.addComponent(downloadAnonymousCheckBox, 0, 3); gridLayout.addComponent(anonymousDownloadAuthenticationConfigurationItem, 1, 3); + final Link linkToSecurityHelp = SPUIComponentProvider + .getHelpLink(uiProperties.getLinks().getDocumentation().getSecurity()); + gridLayout.addComponent(linkToSecurityHelp, 2, 3); + gridLayout.setComponentAlignment(linkToSecurityHelp, Alignment.BOTTOM_RIGHT); + vLayout.addComponent(gridLayout); rootPanel.setContent(vLayout); setCompositionRoot(rootPanel); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/DefaultDistributionSetTypeLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/DefaultDistributionSetTypeLayout.java index 2d09aa00a..23c50d14b 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/DefaultDistributionSetTypeLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/DefaultDistributionSetTypeLayout.java @@ -13,13 +13,13 @@ import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.TenantMetaData; import org.eclipse.hawkbit.ui.SpPermissionChecker; +import org.eclipse.hawkbit.ui.common.builder.LabelBuilder; import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; import org.springframework.data.domain.PageRequest; import com.vaadin.server.FontAwesome; -import com.vaadin.ui.Alignment; import com.vaadin.ui.ComboBox; import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Label; @@ -62,22 +62,21 @@ public class DefaultDistributionSetTypeLayout extends BaseConfigurationView { final VerticalLayout vlayout = new VerticalLayout(); vlayout.setMargin(true); vlayout.setSizeFull(); - final String disSetTypeTitle = i18n.getMessage("configuration.defaultdistributionset.title"); - final Label headerDisSetType = new Label(disSetTypeTitle); - headerDisSetType.addStyleName("config-panel-header"); - vlayout.addComponent(headerDisSetType); + final Label header = new Label(i18n.getMessage("configuration.defaultdistributionset.title")); + header.addStyleName("config-panel-header"); + vlayout.addComponent(header); + final DistributionSetType currentDistributionSetType = getCurrentDistributionSetType(); currentDefaultDisSetType = currentDistributionSetType.getId(); final HorizontalLayout hlayout = new HorizontalLayout(); hlayout.setSpacing(true); - hlayout.setStyleName("config-h-panel"); + hlayout.setImmediate(true); - final Label configurationLabel = new Label( - i18n.getMessage("configuration.defaultdistributionset.select.label")); + final Label configurationLabel = new LabelBuilder() + .name(i18n.getMessage("configuration.defaultdistributionset.select.label")).buildLabel(); hlayout.addComponent(configurationLabel); - hlayout.setComponentAlignment(configurationLabel, Alignment.MIDDLE_LEFT); final Iterable distributionSetTypeCollection = distributionSetTypeManagement .findAll(new PageRequest(0, 100)); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RepositoryConfigurationView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RepositoryConfigurationView.java index de4c0a0d3..a41dc53e8 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RepositoryConfigurationView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RepositoryConfigurationView.java @@ -11,6 +11,7 @@ package org.eclipse.hawkbit.ui.tenantconfiguration; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; import org.eclipse.hawkbit.ui.tenantconfiguration.generic.BooleanConfigurationItem; +import org.eclipse.hawkbit.ui.tenantconfiguration.repository.ActionAutocleanupConfigurationItem; import org.eclipse.hawkbit.ui.tenantconfiguration.repository.ActionAutocloseConfigurationItem; import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; @@ -37,13 +38,19 @@ public class RepositoryConfigurationView extends BaseConfigurationView private final ActionAutocloseConfigurationItem actionAutocloseConfigurationItem; + private final ActionAutocleanupConfigurationItem actionAutocleanupConfigurationItem; + private CheckBox actionAutocloseCheckBox; + private CheckBox actionAutocleanupCheckBox; + RepositoryConfigurationView(final VaadinMessageSource i18n, final TenantConfigurationManagement tenantConfigurationManagement) { this.i18n = i18n; this.actionAutocloseConfigurationItem = new ActionAutocloseConfigurationItem(tenantConfigurationManagement, i18n); + this.actionAutocleanupConfigurationItem = new ActionAutocleanupConfigurationItem(tenantConfigurationManagement, + i18n); init(); } @@ -59,11 +66,11 @@ public class RepositoryConfigurationView extends BaseConfigurationView vLayout.setMargin(true); vLayout.setSizeFull(); - final Label headerDisSetType = new Label(i18n.getMessage("configuration.repository.title")); - headerDisSetType.addStyleName("config-panel-header"); - vLayout.addComponent(headerDisSetType); + final Label header = new Label(i18n.getMessage("configuration.repository.title")); + header.addStyleName("config-panel-header"); + vLayout.addComponent(header); - final GridLayout gridLayout = new GridLayout(2, 1); + final GridLayout gridLayout = new GridLayout(2, 2); gridLayout.setSpacing(true); gridLayout.setImmediate(true); gridLayout.setColumnExpandRatio(1, 1.0F); @@ -77,6 +84,14 @@ public class RepositoryConfigurationView extends BaseConfigurationView gridLayout.addComponent(actionAutocloseCheckBox, 0, 0); gridLayout.addComponent(actionAutocloseConfigurationItem, 1, 0); + actionAutocleanupCheckBox = SPUIComponentProvider.getCheckBox("", DIST_CHECKBOX_STYLE, null, false, ""); + actionAutocleanupCheckBox.setId(UIComponentIdProvider.REPOSITORY_ACTIONS_AUTOCLEANUP_CHECKBOX); + actionAutocleanupCheckBox.setValue(actionAutocleanupConfigurationItem.isConfigEnabled()); + actionAutocleanupCheckBox.addValueChangeListener(this); + actionAutocleanupConfigurationItem.addChangeListener(this); + gridLayout.addComponent(actionAutocleanupCheckBox, 0, 1); + gridLayout.addComponent(actionAutocleanupConfigurationItem, 1, 1); + vLayout.addComponent(gridLayout); rootPanel.setContent(vLayout); setCompositionRoot(rootPanel); @@ -85,12 +100,21 @@ public class RepositoryConfigurationView extends BaseConfigurationView @Override public void save() { actionAutocloseConfigurationItem.save(); + actionAutocleanupConfigurationItem.save(); + } + + @Override + public boolean isUserInputValid() { + return actionAutocloseConfigurationItem.isUserInputValid() + && actionAutocleanupConfigurationItem.isUserInputValid(); } @Override public void undo() { actionAutocloseConfigurationItem.undo(); actionAutocloseCheckBox.setValue(actionAutocloseConfigurationItem.isConfigEnabled()); + actionAutocleanupConfigurationItem.undo(); + actionAutocleanupCheckBox.setValue(actionAutocleanupConfigurationItem.isConfigEnabled()); } @Override @@ -112,6 +136,8 @@ public class RepositoryConfigurationView extends BaseConfigurationView if (actionAutocloseCheckBox.equals(checkBox)) { configurationItem = actionAutocloseConfigurationItem; + } else if (actionAutocleanupCheckBox.equals(checkBox)) { + configurationItem = actionAutocleanupConfigurationItem; } else { return; } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RolloutConfigurationView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RolloutConfigurationView.java index 987ec08b5..b996ed1f0 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RolloutConfigurationView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RolloutConfigurationView.java @@ -8,13 +8,6 @@ */ package org.eclipse.hawkbit.ui.tenantconfiguration; -import com.vaadin.data.Property; -import com.vaadin.ui.CheckBox; -import com.vaadin.ui.GridLayout; -import com.vaadin.ui.Label; -import com.vaadin.ui.Link; -import com.vaadin.ui.Panel; -import com.vaadin.ui.VerticalLayout; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.ui.UiProperties; import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; @@ -22,6 +15,15 @@ import org.eclipse.hawkbit.ui.tenantconfiguration.rollout.ApprovalConfigurationI import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; +import com.vaadin.data.Property; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.CheckBox; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.Link; +import com.vaadin.ui.Panel; +import com.vaadin.ui.VerticalLayout; + /** * Provides configuration of the RolloutManagement including enabling/disabling * of the approval workflow. @@ -55,28 +57,28 @@ public class RolloutConfigurationView extends BaseConfigurationView vLayout.setMargin(true); vLayout.setSizeFull(); - final Label headerDisSetType = new Label(i18n.getMessage("configuration.rollout.title")); - headerDisSetType.addStyleName("config-panel-header"); - vLayout.addComponent(headerDisSetType); + final Label header = new Label(i18n.getMessage("configuration.rollout.title")); + header.addStyleName("config-panel-header"); + vLayout.addComponent(header); - final Link linkToApprovalHelp = SPUIComponentProvider - .getHelpLink(uiProperties.getLinks().getDocumentation().getRollout()); - vLayout.addComponent(linkToApprovalHelp); - - final GridLayout gridLayout = new GridLayout(2, 1); - gridLayout.setSpacing(true); - gridLayout.setImmediate(true); - gridLayout.setColumnExpandRatio(1, 1.0F); + final HorizontalLayout hLayout = new HorizontalLayout(); + hLayout.setSpacing(true); + hLayout.setImmediate(true); approvalCheckbox = SPUIComponentProvider.getCheckBox("", "", null, false, ""); approvalCheckbox.setId(UIComponentIdProvider.ROLLOUT_APPROVAL_ENABLED_CHECKBOX); approvalCheckbox.setValue(approvalConfigurationItem.isConfigEnabled()); approvalCheckbox.addValueChangeListener(this); approvalConfigurationItem.addChangeListener(this); - gridLayout.addComponent(approvalCheckbox, 0, 0); - gridLayout.addComponent(approvalConfigurationItem, 1, 0); + hLayout.addComponent(approvalCheckbox); + hLayout.addComponent(approvalConfigurationItem); - vLayout.addComponent(gridLayout); + final Link linkToApprovalHelp = SPUIComponentProvider + .getHelpLink(uiProperties.getLinks().getDocumentation().getRollout()); + hLayout.addComponent(linkToApprovalHelp); + hLayout.setComponentAlignment(linkToApprovalHelp, Alignment.BOTTOM_RIGHT); + + vLayout.addComponent(hLayout); rootPanel.setContent(vLayout); setCompositionRoot(rootPanel); } @@ -92,7 +94,7 @@ public class RolloutConfigurationView extends BaseConfigurationView } @Override - public void valueChange(Property.ValueChangeEvent event) { + public void valueChange(final Property.ValueChangeEvent event) { if (approvalCheckbox.equals(event.getProperty())) { if (approvalCheckbox.getValue()) { approvalConfigurationItem.configEnable(); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/repository/ActionAutocleanupConfigurationItem.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/repository/ActionAutocleanupConfigurationItem.java new file mode 100644 index 000000000..e16e24043 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/repository/ActionAutocleanupConfigurationItem.java @@ -0,0 +1,321 @@ +/** + * Copyright (c) 2018 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.ui.tenantconfiguration.repository; + +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.ACTION_CLEANUP_ACTION_EXPIRY; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.ACTION_CLEANUP_ACTION_STATUS; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.TenantConfiguration; +import org.eclipse.hawkbit.repository.model.TenantConfigurationValue; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; +import org.eclipse.hawkbit.ui.common.builder.LabelBuilder; +import org.eclipse.hawkbit.ui.common.builder.TextFieldBuilder; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.tenantconfiguration.generic.AbstractBooleanTenantConfigurationItem; +import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; +import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import com.vaadin.data.Validator; +import com.vaadin.data.validator.IntegerRangeValidator; +import com.vaadin.ui.ComboBox; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.TextField; +import com.vaadin.ui.VerticalLayout; + +/** + * This class represents the UI item for configuring automatic action cleanup in + * the Repository Configuration section of the System Configuration view. + */ +public class ActionAutocleanupConfigurationItem extends AbstractBooleanTenantConfigurationItem { + + private static final long serialVersionUID = 1L; + + private static final Logger LOGGER = LoggerFactory.getLogger(ActionAutocleanupConfigurationItem.class); + + private static final int MAX_EXPIRY_IN_DAYS = 1000; + private static final EnumSet EMPTY_STATUS_SET = EnumSet.noneOf(Status.class); + + private static final String MSG_KEY_PREFIX = "label.configuration.repository.autocleanup.action.prefix"; + private static final String MSG_KEY_BODY = "label.configuration.repository.autocleanup.action.body"; + private static final String MSG_KEY_SUFFIX = "label.configuration.repository.autocleanup.action.suffix"; + private static final String MSG_KEY_INVALID_EXPIRY = "label.configuration.repository.autocleanup.action.expiry.invalid"; + private static final String MSG_KEY_NOTICE = "label.configuration.repository.autocleanup.action.notice"; + + private static final Collection ACTION_STATUS_OPTIONS = Arrays.asList( + new ActionStatusOption(Status.CANCELED), new ActionStatusOption(Status.ERROR), + new ActionStatusOption(Status.CANCELED, Status.ERROR)); + + private final VerticalLayout container; + private final ComboBox actionStatusCombobox; + private final TextField actionExpiryInput; + + private final VaadinMessageSource i18n; + + private boolean cleanupEnabled; + private boolean cleanupEnabledChanged; + private boolean actionStatusChanged; + private boolean actionExpiryChanged; + + /** + * Constructs the Action Cleanup configuration UI. + * + * @param tenantConfigurationManagement + * Configuration service to read /write tenant-specific + * configuration settings. + * @param i18n + * The resource bundle to get all localized strings from. + */ + public ActionAutocleanupConfigurationItem(final TenantConfigurationManagement tenantConfigurationManagement, + final VaadinMessageSource i18n) { + super(TenantConfigurationKey.ACTION_CLEANUP_ENABLED, tenantConfigurationManagement, i18n); + super.init("label.configuration.repository.autocleanup.action"); + + this.i18n = i18n; + cleanupEnabled = isConfigEnabled(); + + container = new VerticalLayout(); + container.setImmediate(true); + + final HorizontalLayout row1 = newHorizontalLayout(); + + actionStatusCombobox = SPUIComponentProvider.getComboBox(null, "200", null, null, false, "", + "label.combobox.action.status.options"); + actionStatusCombobox.setId(UIComponentIdProvider.SYSTEM_CONFIGURATION_ACTION_CLEANUP_ACTION_TYPES); + actionStatusCombobox.setNullSelectionAllowed(false); + + for (final ActionStatusOption statusOption : ACTION_STATUS_OPTIONS) { + actionStatusCombobox.addItem(statusOption); + actionStatusCombobox.setItemCaption(statusOption, statusOption.getName()); + } + actionStatusCombobox.setImmediate(true); + actionStatusCombobox.addValueChangeListener(e -> onActionStatusChanged()); + actionStatusCombobox.select(getActionStatusOption()); + + actionExpiryInput = new TextFieldBuilder(TenantConfiguration.VALUE_MAX_SIZE).buildTextComponent(); + actionExpiryInput.setId(UIComponentIdProvider.SYSTEM_CONFIGURATION_ACTION_CLEANUP_ACTION_EXPIRY); + actionExpiryInput.setWidth(55, Unit.PIXELS); + actionExpiryInput.setNullSettingAllowed(false); + actionExpiryInput.addTextChangeListener(e -> onActionExpiryChanged()); + actionExpiryInput.addValidator(new ActionExpiryValidator(i18n.getMessage(MSG_KEY_INVALID_EXPIRY))); + actionExpiryInput.setValue(String.valueOf(getActionExpiry())); + + row1.addComponent(newLabel(MSG_KEY_PREFIX)); + row1.addComponent(actionStatusCombobox); + row1.addComponent(newLabel(MSG_KEY_BODY)); + row1.addComponent(actionExpiryInput); + row1.addComponent(newLabel(MSG_KEY_SUFFIX)); + container.addComponent(row1); + + final HorizontalLayout row2 = newHorizontalLayout(); + row2.addComponent(newLabel(MSG_KEY_NOTICE)); + container.addComponent(row2); + + if (isConfigEnabled()) { + setSettingsVisible(true); + } + + } + + @Override + public void configEnable() { + if (!cleanupEnabled) { + cleanupEnabledChanged = true; + } + cleanupEnabled = true; + setSettingsVisible(true); + } + + @Override + public void configDisable() { + if (cleanupEnabled) { + cleanupEnabledChanged = true; + } + cleanupEnabled = false; + setSettingsVisible(false); + } + + @Override + public void save() { + if (cleanupEnabledChanged) { + setActionCleanupEnabled(cleanupEnabled); + cleanupEnabledChanged = false; + } + if (cleanupEnabled && actionStatusChanged) { + setActionStatus((ActionStatusOption) actionStatusCombobox.getValue()); + actionStatusChanged = false; + } + if (cleanupEnabled && actionExpiryChanged) { + setActionExpiry(Long.parseLong(actionExpiryInput.getValue())); + actionExpiryChanged = false; + } + } + + @Override + public boolean isUserInputValid() { + return actionExpiryInput.getErrorMessage() == null; + } + + @Override + public void undo() { + cleanupEnabledChanged = false; + cleanupEnabled = readConfigValue(getConfigurationKey(), Boolean.class).getValue(); + actionStatusChanged = false; + actionStatusCombobox.select(getActionStatusOption()); + actionExpiryChanged = false; + actionExpiryInput.setValue(String.valueOf(getActionExpiry())); + } + + private void onActionExpiryChanged() { + actionExpiryChanged = true; + notifyConfigurationChanged(); + } + + private void onActionStatusChanged() { + actionStatusChanged = true; + notifyConfigurationChanged(); + } + + private Label newLabel(final String msgKey) { + final Label label = new LabelBuilder().name(i18n.getMessage(msgKey)).buildLabel(); + label.setWidthUndefined(); + return label; + } + + private static HorizontalLayout newHorizontalLayout() { + final HorizontalLayout layout = new HorizontalLayout(); + layout.setSpacing(true); + layout.setImmediate(true); + return layout; + } + + private void setSettingsVisible(final boolean visible) { + if (visible) { + addComponent(container); + } else { + removeComponent(container); + } + } + + private void setActionCleanupEnabled(final boolean enabled) { + writeConfigValue(getConfigurationKey(), enabled); + } + + private void setActionExpiry(final long days) { + writeConfigValue(ACTION_CLEANUP_ACTION_EXPIRY, TimeUnit.DAYS.toMillis(days)); + } + + private long getActionExpiry() { + return TimeUnit.MILLISECONDS.toDays(readConfigValue(ACTION_CLEANUP_ACTION_EXPIRY, Long.class).getValue()); + } + + private void setActionStatus(final ActionStatusOption statusOption) { + setActionStatus(statusOption.getStatus()); + } + + private void setActionStatus(final Set status) { + writeConfigValue(ACTION_CLEANUP_ACTION_STATUS, + status.stream().map(Status::name).collect(Collectors.joining(","))); + } + + private ActionStatusOption getActionStatusOption() { + final Set actionStatus = getActionStatus(); + return ACTION_STATUS_OPTIONS.stream().filter(option -> actionStatus.equals(option.getStatus())).findFirst() + .orElse(ACTION_STATUS_OPTIONS.iterator().next()); + } + + private EnumSet getActionStatus() { + final TenantConfigurationValue statusStr = readConfigValue(ACTION_CLEANUP_ACTION_STATUS, String.class); + if (statusStr != null) { + return Arrays.stream(statusStr.getValue().split("[;,]")).map(Status::valueOf) + .collect(Collectors.toCollection(() -> EnumSet.noneOf(Status.class))); + } + return EMPTY_STATUS_SET; + } + + private TenantConfigurationValue readConfigValue(final String key, + final Class valueType) { + return getTenantConfigurationManagement().getConfigurationValue(key, valueType); + } + + private void writeConfigValue(final String key, final T value) { + getTenantConfigurationManagement().addOrUpdateConfiguration(key, value); + } + + private static class ActionStatusOption { + + private static final CharSequence SEPARATOR = " + "; + private final Set statusSet; + private String name; + + public ActionStatusOption(final Status... status) { + statusSet = Arrays.stream(status).collect(Collectors.toCollection(() -> EnumSet.noneOf(Status.class))); + } + + public String getName() { + if (name == null) { + name = assembleName(); + } + return name; + } + + public Set getStatus() { + return statusSet; + } + + private String assembleName() { + return statusSet.stream().map(Status::name).collect(Collectors.joining(SEPARATOR)); + } + + } + + static class ActionExpiryValidator implements Validator { + + private static final long serialVersionUID = 1L; + + private final String message; + + private final Validator rangeValidator; + + ActionExpiryValidator(final String message) { + this.message = message; + this.rangeValidator = new IntegerRangeValidator(message, 1, MAX_EXPIRY_IN_DAYS); + } + + @Override + public void validate(final Object value) { + + if (StringUtils.isEmpty(value)) { + throw new InvalidValueException(message); + } + + try { + rangeValidator.validate(Integer.parseInt(value.toString())); + } catch (final RuntimeException e) { + LOGGER.debug("Action expiry validation failed", e); + throw new InvalidValueException(message); + } + } + + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java index 4d01ab4a6..9ddf23416 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java @@ -519,6 +519,16 @@ public final class UIComponentIdProvider { */ public static final String SYSTEM_CONFIGURATION_SAVE = "system.configuration.save"; + /** + * Combobox for action types + */ + public static final String SYSTEM_CONFIGURATION_ACTION_CLEANUP_ACTION_TYPES = "system.configuration.autocleanup.action.types"; + + /** + * Combobox for action expiry in days + */ + public static final String SYSTEM_CONFIGURATION_ACTION_CLEANUP_ACTION_EXPIRY = "system.configuration.autocleanup.action.expiry"; + /** * ID for save button in pop-up-windows instance of commonDialogWindow */ @@ -1200,6 +1210,12 @@ public final class UIComponentIdProvider { */ public static final String REPOSITORY_ACTIONS_AUTOCLOSE_CHECKBOX = "repositoryactionsautoclosecheckbox"; + /** + * Configuration checkbox for + * {@link TenantConfigurationKey#REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED}. + */ + public static final String REPOSITORY_ACTIONS_AUTOCLEANUP_CHECKBOX = "repositoryactionsautocleanupcheckbox"; + /** * Configuration checkbox for * {@link TenantConfigurationKey#ROLLOUT_APPROVAL_ENABLED} diff --git a/hawkbit-ui/src/main/resources/messages.properties b/hawkbit-ui/src/main/resources/messages.properties index b45ccdc3c..2d842a91e 100644 --- a/hawkbit-ui/src/main/resources/messages.properties +++ b/hawkbit-ui/src/main/resources/messages.properties @@ -230,12 +230,18 @@ label.tag.name = Tag name label.configuration.auth.header = Allow targets to authenticate via a certificate authenticated by an reverse proxy label.configuration.auth.gatewaytoken = Allow a gateway to authenticate and manage multiple targets through a gateway security token label.configuration.auth.targettoken = Allow targets to authenticate directly with their target security token -label.configuration.repository.autoclose.action = Autoclose running actions with new Distribution set assignment +label.configuration.repository.autoclose.action = Autoclose running actions when a new distribution set is assigned +label.configuration.repository.autocleanup.action = Automatically delete terminated actions +label.configuration.repository.autocleanup.action.prefix = Delete actions with status +label.configuration.repository.autocleanup.action.body = after +label.configuration.repository.autocleanup.action.suffix = day(s) +label.configuration.repository.autocleanup.action.expiry.invalid = The specified number of days is invalid. Please enter a positive integer value between 1 and 1000. label.configuration.anonymous.download = Allow targets to download artifacts without security credentials -label.unsupported.browser.ie=Sorry! current browser is not supported. Please use Internet Explorer 11 and above -label.auto.assign.description=When an auto assign distribution set is selected, it will be automatically assigned to all targets that match the target filter. -label.auto.assign.enable=Enable auto assignment -label.scheduled=Scheduled +label.configuration.repository.autocleanup.action.notice = Warning: The actions are deleted from the repository and cannot be restored +label.unsupported.browser.ie = Sorry! Your current browser is not supported. Please use Internet Explorer 11 and above +label.auto.assign.description = When an auto assign distribution set is selected, it will be automatically assigned to all targets that match the target filter. +label.auto.assign.enable = Enable auto assignment +label.scheduled = Scheduled label.approval.decision = Approval decision label.approval.remark = Remark (optional) label.drop.area.upload = Drop Files to upload @@ -509,8 +515,8 @@ link.usermanagement.name=User Management # System Configuration View notification.configuration.save.successful=Saved changes notification.configuration.save.notpossible = Saving was not possible, because of invalid user input. -configuration.defaultdistributionset.title=Distribution set type -configuration.defaultdistributionset.select.label=Select the default Distribution set type: +configuration.defaultdistributionset.title=Distribution Configuration +configuration.defaultdistributionset.select.label=Select the default distribution set type: configuration.savebutton.tooltip=Save Configurations configuration.cancellbutton.tooltip=Cancel Configurations configuration.authentication.title=Authentication Configuration