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 <stefan.behl@bosch-si.com>
This commit is contained in:
Stefan Behl
2018-07-19 15:23:14 +02:00
committed by GitHub
parent 5e9b0fb884
commit 19caff6e46
28 changed files with 1165 additions and 83 deletions

View File

@@ -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<DistributionSet> 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<Action.Status> status, long lastModified);
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -63,7 +63,7 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
* the {@link DistributionSet} on which will be filtered
* @return the found {@link Action}s
*/
Page<Action> findByDistributionSetId(final Pageable pageable, final Long dsId);
Page<Action> findByDistributionSetId(Pageable pageable, Long dsId);
/**
* Retrieves all {@link Action}s which are referring the given
@@ -98,7 +98,7 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
* the action active flag
* @return the found {@link Action}s
*/
List<Action> findByTargetAndActiveOrderByIdAsc(final JpaTarget target, boolean active);
List<Action> 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<JpaAction, Long>,
* @return the found {@link Action}
*/
@EntityGraph(value = "Action.ds", type = EntityGraphType.LOAD)
Optional<Action> findFirstByTargetControllerIdAndActive(final Sort sort, final String controllerId, boolean active);
Optional<Action> findFirstByTargetControllerIdAndActive(Sort sort, String controllerId, boolean active);
/**
* Checks if an active action exists for given
@@ -139,8 +139,7 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
* 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<Action> findActionByTargetAndSoftwareModule(@Param("target") final String targetId,
@Param("module") Long moduleId);
List<Action> 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<JpaAction, Long>,
* @return the found {@link Action}s
*/
@Query("Select a from JpaAction a where a.target = :target and a.distributionSet = :ds")
Page<JpaAction> findByTargetAndDistributionSet(final Pageable pageable, @Param("target") final JpaTarget target,
Page<JpaAction> findByTargetAndDistributionSet(Pageable pageable, @Param("target") JpaTarget target,
@Param("ds") JpaDistributionSet ds);
/**
@@ -453,5 +452,6 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
@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<Long> actionIDs);
void deleteByIdIn(Collection<Long> actionIDs);
}

View File

@@ -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<String> findMessagesByActionIdAndMessageNotLike(final Pageable pageable, @Param("actionId") Long actionId,
Page<String> findMessagesByActionIdAndMessageNotLike(Pageable pageable, @Param("actionId") Long actionId,
@Param("filter") String filter);
}

View File

@@ -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<JpaDistributionSet> findByTag(Pageable pageable, @Param("tag") final Long tagId);
Page<JpaDistributionSet> findByTag(Pageable pageable, @Param("tag") Long tagId);
/**
* deletes the {@link DistributionSet}s with the given IDs.

View File

@@ -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<DistributionSetTag> findByNameEquals(final String tagName);
Optional<DistributionSetTag> findByNameEquals(String tagName);
/**
* Checks if tag with given name exists.

View File

@@ -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> 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<String> elements) {
return "#" + Joiner.on(",#").join(elements);
}
}

View File

@@ -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<CleanupTask> cleanupTasks) {
return new AutoCleanupScheduler(systemManagement, systemSecurityContext, lockRegistry, cleanupTasks);
}
/**
* {@link RolloutScheduler} bean.
*

View File

@@ -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<JpaRolloutGroup> findByRolloutId(final Long rolloutId, Pageable page);
Page<JpaRolloutGroup> 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")

View File

@@ -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);
}

View File

@@ -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

View File

@@ -36,7 +36,7 @@ public interface TargetFilterQueryRepository
* @param name
* @return custom target filter
*/
Optional<TargetFilterQuery> findByName(final String name);
Optional<TargetFilterQuery> findByName(String name);
/**
* Find list of all custom target filters.

View File

@@ -112,7 +112,7 @@ public interface TargetRepository extends BaseEntityRepository<JpaTarget, Long>,
@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<Long> targetIDs);
void deleteByIdIn(Collection<Long> targetIDs);
/**
* Finds {@link Target}s by assigned {@link Tag}.
@@ -125,7 +125,7 @@ public interface TargetRepository extends BaseEntityRepository<JpaTarget, Long>,
* @return page of found targets
*/
@Query(value = "SELECT DISTINCT t FROM JpaTarget t JOIN t.tags tt WHERE tt.id = :tag")
Page<JpaTarget> findByTag(Pageable page, @Param("tag") final Long tagId);
Page<JpaTarget> 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<JpaTarget, Long>,
* @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<JpaTarget> findByTagNameAndControllerIdIn(@Param("tagname") final String tag,
@Param("targets") final Collection<String> controllerIds);
List<JpaTarget> findByTagNameAndControllerIdIn(@Param("tagname") String tag,
@Param("targets") Collection<String> controllerIds);
/**
* Used by UI to filter based on selected status.
@@ -151,7 +151,7 @@ public interface TargetRepository extends BaseEntityRepository<JpaTarget, Long>,
*
* @return found targets
*/
Page<Target> findByUpdateStatus(final Pageable pageable, final TargetUpdateStatus status);
Page<Target> findByUpdateStatus(Pageable pageable, TargetUpdateStatus status);
/**
* retrieves the {@link Target}s which has the {@link DistributionSet}
@@ -163,7 +163,7 @@ public interface TargetRepository extends BaseEntityRepository<JpaTarget, Long>,
* the ID of the {@link DistributionSet}
* @return the found {@link Target}s
*/
Page<Target> findByInstalledDistributionSetId(final Pageable pageable, final Long setID);
Page<Target> findByInstalledDistributionSetId(Pageable pageable, Long setID);
/**
* Finds all targets that have defined {@link DistributionSet} assigned.
@@ -175,7 +175,7 @@ public interface TargetRepository extends BaseEntityRepository<JpaTarget, Long>,
*
* @return page of found targets
*/
Page<Target> findByAssignedDistributionSetId(final Pageable pageable, final Long setID);
Page<Target> findByAssignedDistributionSetId(Pageable pageable, Long setID);
/**
* Counts number of targets with given distribution set Id.
@@ -185,7 +185,7 @@ public interface TargetRepository extends BaseEntityRepository<JpaTarget, Long>,
*
* @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<JpaTarget, Long>,
* 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<JpaTarget, Long>,
* the page request parameter
* @return a page of all targets related to a rollout group
*/
Page<Target> findByRolloutTargetGroupRolloutGroupId(final Long rolloutGroupId, Pageable page);
Page<Target> findByRolloutTargetGroupRolloutGroupId(Long rolloutGroupId, Pageable page);
/**
* Finds all targets related to a target rollout group stored for a specific

View File

@@ -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<Status> 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> 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<Long> expiry = getConfigValue(ACTION_CLEANUP_ACTION_EXPIRY, Long.class);
return expiry != null ? expiry.getValue() : ACTION_CLEANUP_ACTION_EXPIRY_DEFAULT;
}
private EnumSet<Status> getActionStatus() {
final TenantConfigurationValue<String> 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<Boolean> isEnabled = getConfigValue(ACTION_CLEANUP_ENABLED, Boolean.class);
return isEnabled != null ? isEnabled.getValue() : ACTION_CLEANUP_ENABLED_DEFAULT;
}
private <T extends Serializable> TenantConfigurationValue<T> getConfigValue(final String key,
final Class<T> valueType) {
return config.getConfigurationValue(key, valueType);
}
}

View File

@@ -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<CleanupTask> 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<CleanupTask> 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);
}
}

View File

@@ -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();
}

View File

@@ -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(",")));
}
}

View File

@@ -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";
}
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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<DistributionSetType> distributionSetTypeCollection = distributionSetTypeManagement
.findAll(new PageRequest(0, 100));

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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<Status> 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<ActionStatusOption> 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> status) {
writeConfigValue(ACTION_CLEANUP_ACTION_STATUS,
status.stream().map(Status::name).collect(Collectors.joining(",")));
}
private ActionStatusOption getActionStatusOption() {
final Set<Status> actionStatus = getActionStatus();
return ACTION_STATUS_OPTIONS.stream().filter(option -> actionStatus.equals(option.getStatus())).findFirst()
.orElse(ACTION_STATUS_OPTIONS.iterator().next());
}
private EnumSet<Status> getActionStatus() {
final TenantConfigurationValue<String> 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 <T extends Serializable> TenantConfigurationValue<T> readConfigValue(final String key,
final Class<T> valueType) {
return getTenantConfigurationManagement().getConfigurationValue(key, valueType);
}
private <T extends Serializable> void writeConfigValue(final String key, final T value) {
getTenantConfigurationManagement().addOrUpdateConfiguration(key, value);
}
private static class ActionStatusOption {
private static final CharSequence SEPARATOR = " + ";
private final Set<Status> 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<Status> 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);
}
}
}
}

View File

@@ -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}

View File

@@ -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