Feature Approval Workflow for rollouts (#678)
* implement feature approval workflow Signed-off-by: Ioannis Spyropoulos <ioannis.spyropoulos@bosch-si.com> Signed-off-by: Michael Müller <Michael.Mueller17@bosch-si.com> * add documentation for REST endpoints Signed-off-by: Ioannis Spyropoulos <ioannis.spyropoulos@bosch-si.com> Signed-off-by: Michael Müller <Michael.Mueller17@bosch-si.com> * fix broken documentation test Signed-off-by: Ioannis Spyropoulos <ioannis.spyropoulos@bosch-si.com> * fix broken documentation test II Signed-off-by: Ioannis Spyropoulos <ioannis.spyropoulos@bosch-si.com>
This commit is contained in:
committed by
Dominic Schabel
parent
8a14c931c8
commit
cef7c2bbf2
@@ -340,6 +340,54 @@ public interface RolloutManagement {
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_HANDLE)
|
||||
void resumeRollout(long rolloutId);
|
||||
|
||||
/**
|
||||
* Approves or denies a created rollout being in state
|
||||
* {@link RolloutStatus#WAITING_FOR_APPROVAL}. If the rollout is approved, it
|
||||
* switches state to {@link RolloutStatus#READY}, otherwise it switches to state
|
||||
* {@link RolloutStatus#APPROVAL_DENIED}
|
||||
*
|
||||
* @param rolloutId
|
||||
* the rollout to be approved or denied.
|
||||
* @param decision
|
||||
* decision whether a rollout is approved or denied.
|
||||
*
|
||||
* @return approved or denied rollout
|
||||
*
|
||||
* @throws EntityNotFoundException
|
||||
* if rollout with given ID does not exist
|
||||
* @throws RolloutIllegalStateException
|
||||
* if given rollout is not in
|
||||
* {@link RolloutStatus#WAITING_FOR_APPROVAL}. Only rollouts
|
||||
* waiting for approval can be acted upon.
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_APPROVE)
|
||||
Rollout approveOrDeny(long rolloutId, Rollout.ApprovalDecision decision);
|
||||
|
||||
/**
|
||||
* Approves or denies a created rollout being in state
|
||||
* {@link RolloutStatus#WAITING_FOR_APPROVAL}. If the rollout is approved, it
|
||||
* switches state to {@link RolloutStatus#READY}, otherwise it switches to state
|
||||
* {@link RolloutStatus#APPROVAL_DENIED}
|
||||
*
|
||||
* @param rolloutId
|
||||
* the rollout to be approved or denied.
|
||||
* @param decision
|
||||
* decision whether a rollout is approved or denied.
|
||||
* @param remark
|
||||
* user remark on approve / deny decision
|
||||
*
|
||||
* @return approved or denied rollout
|
||||
*
|
||||
* @throws EntityNotFoundException
|
||||
* if rollout with given ID does not exist
|
||||
* @throws RolloutIllegalStateException
|
||||
* if given rollout is not in
|
||||
* {@link RolloutStatus#WAITING_FOR_APPROVAL}. Only rollouts
|
||||
* waiting for approveOrDeny can be acted upon.
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_APPROVE)
|
||||
Rollout approveOrDeny(long rolloutId, Rollout.ApprovalDecision decision, String remark);
|
||||
|
||||
/**
|
||||
* Starts a rollout which has been created. The rollout must be in
|
||||
* {@link RolloutStatus#READY} state. The Rollout will be set into the
|
||||
|
||||
@@ -25,6 +25,17 @@ import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus.Status;
|
||||
*/
|
||||
public interface Rollout extends NamedEntity {
|
||||
|
||||
/**
|
||||
* Maximum length of author name.
|
||||
*/
|
||||
int APPROVED_BY_MAX_SIZE = 40;
|
||||
|
||||
|
||||
/**
|
||||
* Maximum length on comment regarding approval decision.
|
||||
*/
|
||||
int APPROVAL_REMARK_MAX_SIZE = 255;
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if the rollout is deleted and only kept for
|
||||
* history purposes.
|
||||
@@ -81,6 +92,16 @@ public interface Rollout extends NamedEntity {
|
||||
*/
|
||||
TotalTargetCountStatus getTotalTargetCountStatus();
|
||||
|
||||
/**
|
||||
* @return user that approved or denied the {@link Rollout}.
|
||||
*/
|
||||
String getApprovalDecidedBy();
|
||||
|
||||
/**
|
||||
* @return additional note on approval/denial decision.
|
||||
*/
|
||||
String getApprovalRemark();
|
||||
|
||||
/**
|
||||
*
|
||||
* State machine for rollout.
|
||||
@@ -93,6 +114,16 @@ public interface Rollout extends NamedEntity {
|
||||
*/
|
||||
CREATING,
|
||||
|
||||
/**
|
||||
* Rollout needs to be approved.
|
||||
*/
|
||||
WAITING_FOR_APPROVAL,
|
||||
|
||||
/**
|
||||
* Rollout approval is denied. Can not be started.
|
||||
*/
|
||||
APPROVAL_DENIED,
|
||||
|
||||
/**
|
||||
* Rollout is ready to start.
|
||||
*/
|
||||
@@ -154,4 +185,18 @@ public interface Rollout extends NamedEntity {
|
||||
ERROR_STARTING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum that holds all possible approval workflow decisions.
|
||||
*/
|
||||
enum ApprovalDecision {
|
||||
/**
|
||||
* Representing an granted approval for a rollout.
|
||||
*/
|
||||
APPROVED,
|
||||
/**
|
||||
* Representing a rejected rollout.
|
||||
*/
|
||||
DENIED
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -114,6 +114,11 @@ public class TenantConfigurationProperties {
|
||||
*/
|
||||
public static final String ANONYMOUS_DOWNLOAD_MODE_ENABLED = "anonymous.download.enabled";
|
||||
|
||||
/**
|
||||
* Represents setting if approval for a rollout is needed.
|
||||
*/
|
||||
public static final String ROLLOUT_APPROVAL_ENABLED = "rollout.approval.enabled";
|
||||
|
||||
/**
|
||||
* Repository on autoclose mode instead of canceling in case of new DS
|
||||
* assignment over active actions.
|
||||
|
||||
@@ -59,12 +59,14 @@ public abstract class AbstractRolloutManagement implements RolloutManagement {
|
||||
|
||||
protected final LockRegistry lockRegistry;
|
||||
|
||||
protected final RolloutApprovalStrategy rolloutApprovalStrategy;
|
||||
|
||||
protected AbstractRolloutManagement(final TargetManagement targetManagement,
|
||||
final DeploymentManagement deploymentManagement, final RolloutGroupManagement rolloutGroupManagement,
|
||||
final DistributionSetManagement distributionSetManagement, final ApplicationContext context,
|
||||
final ApplicationEventPublisher eventPublisher, final VirtualPropertyReplacer virtualPropertyReplacer,
|
||||
final PlatformTransactionManager txManager, final TenantAware tenantAware,
|
||||
final LockRegistry lockRegistry) {
|
||||
final LockRegistry lockRegistry, final RolloutApprovalStrategy rolloutApprovalStrategy) {
|
||||
this.targetManagement = targetManagement;
|
||||
this.deploymentManagement = deploymentManagement;
|
||||
this.rolloutGroupManagement = rolloutGroupManagement;
|
||||
@@ -75,6 +77,7 @@ public abstract class AbstractRolloutManagement implements RolloutManagement {
|
||||
this.txManager = txManager;
|
||||
this.tenantAware = tenantAware;
|
||||
this.lockRegistry = lockRegistry;
|
||||
this.rolloutApprovalStrategy = rolloutApprovalStrategy;
|
||||
}
|
||||
|
||||
protected Long runInNewTransaction(final String transactionName, final TransactionCallback<Long> action) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||
|
||||
/**
|
||||
* This interface provides methods to enable plugging in of different strategies
|
||||
* to handle rollout approval.
|
||||
*/
|
||||
public interface RolloutApprovalStrategy {
|
||||
|
||||
/**
|
||||
* This method handles whether a rollout needs approval. Various factors may be
|
||||
* important according to the implementation, e.g. user roles of the rollout
|
||||
* creator, state of the system, ....
|
||||
*
|
||||
* @param rollout
|
||||
* rollout to decide for whether approval is needed.
|
||||
* @return true if the rollout according to this strategy needs approval.
|
||||
*/
|
||||
boolean isApprovalNeeded(Rollout rollout);
|
||||
|
||||
/**
|
||||
* Depending on the implementation, creation of a approval task,
|
||||
* notification,... inside or outside of hawkbit may be necessary.
|
||||
* Implementations may also decide to provide an empty implementation for this
|
||||
* method.
|
||||
*
|
||||
* @param rollout
|
||||
* rollout to create approval task for.
|
||||
*/
|
||||
void onApprovalRequired(Rollout rollout);
|
||||
|
||||
/**
|
||||
* Returns the user that made a decision to approve or deny the given rollout.
|
||||
* Depending on the implementation this may be different to the current user eg.
|
||||
* when the decision is made in an external system.
|
||||
*
|
||||
* @param rollout
|
||||
* target rollout
|
||||
* @return identifier of the user that decided on approval
|
||||
*/
|
||||
String getApprovalUser(Rollout rollout);
|
||||
}
|
||||
@@ -71,4 +71,10 @@ hawkbit.server.tenant.configuration.anonymous-download-enabled.keyName=anonymous
|
||||
hawkbit.server.tenant.configuration.anonymous-download-enabled.defaultValue=${hawkbit.server.download.anonymous.enabled}
|
||||
hawkbit.server.tenant.configuration.anonymous-download-enabled.dataType=java.lang.Boolean
|
||||
hawkbit.server.tenant.configuration.anonymous-download-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator
|
||||
|
||||
hawkbit.server.tenant.configuration.rollout-approval-enabled.keyName=rollout.approval.enabled
|
||||
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
|
||||
|
||||
# Default tenant configuration - END
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission;
|
||||
import org.eclipse.hawkbit.im.authentication.UserPrincipal;
|
||||
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
|
||||
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
||||
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
||||
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link RolloutApprovalStrategy}. Decides whether
|
||||
* approval is needed based on configuration of the tenant as well as the roles
|
||||
* of the user who created the Rollout. Provides a no-operation implementation
|
||||
* of {@link RolloutApprovalStrategy#onApprovalRequired(Rollout)}.
|
||||
*/
|
||||
public class DefaultRolloutApprovalStrategy implements RolloutApprovalStrategy {
|
||||
|
||||
private final UserDetailsService userDetailsService;
|
||||
|
||||
private final TenantConfigurationManagement tenantConfigurationManagement;
|
||||
|
||||
private final SystemSecurityContext systemSecurityContext;
|
||||
|
||||
DefaultRolloutApprovalStrategy(UserDetailsService userDetailsService,
|
||||
TenantConfigurationManagement tenantConfigurationManagement,
|
||||
SystemSecurityContext systemSecurityContext) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.tenantConfigurationManagement = tenantConfigurationManagement;
|
||||
this.systemSecurityContext = systemSecurityContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true, if rollout approval is enabled and rollout creator doesn't have
|
||||
* approval role.
|
||||
*/
|
||||
@Override
|
||||
public boolean isApprovalNeeded(final Rollout rollout) {
|
||||
final UserDetails userDetails = this.getActor(rollout);
|
||||
final boolean approvalEnabled = this.tenantConfigurationManagement
|
||||
.getConfigurationValue(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, Boolean.class).getValue();
|
||||
return approvalEnabled && userDetails.getAuthorities().stream()
|
||||
.noneMatch(authority -> SpPermission.APPROVE_ROLLOUT.equals(authority.getAuthority()));
|
||||
}
|
||||
|
||||
|
||||
private UserDetails getActor(Rollout rollout) {
|
||||
final String actor = rollout.getLastModifiedBy() != null ? rollout.getLastModifiedBy() : rollout.getCreatedBy();
|
||||
return systemSecurityContext.runAsSystem(() -> {
|
||||
UserPrincipal userPrincipal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
if(userPrincipal.getUsername().equals(actor)) {
|
||||
return userPrincipal;
|
||||
} else {
|
||||
return this.userDetailsService.loadUserByUsername(actor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/***
|
||||
* Per default do nothing.
|
||||
*
|
||||
* @param rollout
|
||||
* rollout to create approval task for.
|
||||
*/
|
||||
@Override
|
||||
public void onApprovalRequired(Rollout rollout) {
|
||||
// do nothing per default, can be extended by further implementations.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApprovalUser(Rollout rollout) {
|
||||
return SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import org.eclipse.hawkbit.repository.AbstractRolloutManagement;
|
||||
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetManagement;
|
||||
import org.eclipse.hawkbit.repository.QuotaManagement;
|
||||
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
|
||||
import org.eclipse.hawkbit.repository.RolloutFields;
|
||||
import org.eclipse.hawkbit.repository.RolloutGroupManagement;
|
||||
import org.eclipse.hawkbit.repository.RolloutHelper;
|
||||
@@ -157,9 +158,9 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
|
||||
final DistributionSetManagement distributionSetManagement, final ApplicationContext context,
|
||||
final ApplicationEventPublisher eventPublisher, final VirtualPropertyReplacer virtualPropertyReplacer,
|
||||
final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry,
|
||||
final Database database) {
|
||||
final Database database, final RolloutApprovalStrategy rolloutApprovalStrategy) {
|
||||
super(targetManagement, deploymentManagement, rolloutGroupManagement, distributionSetManagement, context,
|
||||
eventPublisher, virtualPropertyReplacer, txManager, tenantAware, lockRegistry);
|
||||
eventPublisher, virtualPropertyReplacer, txManager, tenantAware, lockRegistry, rolloutApprovalStrategy);
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
@@ -357,8 +358,14 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
|
||||
// When all groups are ready the rollout status can be changed to be
|
||||
// ready, too.
|
||||
if (readyGroups == rolloutGroups.size()) {
|
||||
LOGGER.debug("rollout {} creation done. Switch to READY.", rollout.getId());
|
||||
rollout.setStatus(RolloutStatus.READY);
|
||||
if (!rolloutApprovalStrategy.isApprovalNeeded(rollout)) {
|
||||
rollout.setStatus(RolloutStatus.READY);
|
||||
LOGGER.debug("rollout {} creation done. Switch to READY.", rollout.getId());
|
||||
} else {
|
||||
LOGGER.debug("rollout {} creation done. Switch to WAITING_FOR_APPROVAL.", rollout.getId());
|
||||
rollout.setStatus(RolloutStatus.WAITING_FOR_APPROVAL);
|
||||
rolloutApprovalStrategy.onApprovalRequired(rollout);
|
||||
}
|
||||
rollout.setLastCheck(0);
|
||||
rollout.setTotalTargets(totalTargets);
|
||||
rolloutRepository.save(rollout);
|
||||
@@ -451,6 +458,39 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
|
||||
groups.stream().map(RolloutGroupCreate::build).collect(Collectors.toList()), baseFilter, totalTargets));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(include = {
|
||||
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
||||
public Rollout approveOrDeny(final long rolloutId, final Rollout.ApprovalDecision decision) {
|
||||
return this.approveOrDeny(rolloutId, decision, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(include = {
|
||||
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
||||
public Rollout approveOrDeny(final long rolloutId, final Rollout.ApprovalDecision decision, final String remark) {
|
||||
LOGGER.debug("approveOrDeny rollout called for rollout {} with decision {}", rolloutId, decision);
|
||||
final JpaRollout rollout = getRolloutAndThrowExceptionIfNotFound(rolloutId);
|
||||
RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.WAITING_FOR_APPROVAL);
|
||||
switch (decision) {
|
||||
case APPROVED:
|
||||
rollout.setStatus(RolloutStatus.READY);
|
||||
break;
|
||||
case DENIED:
|
||||
rollout.setStatus(RolloutStatus.APPROVAL_DENIED);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown approval decision: " + decision);
|
||||
}
|
||||
rollout.setApprovalDecidedBy(rolloutApprovalStrategy.getApprovalUser(rollout));
|
||||
if (remark != null) {
|
||||
rollout.setApprovalRemark(remark);
|
||||
}
|
||||
return rolloutRepository.save(rollout);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(include = {
|
||||
@@ -964,7 +1004,6 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
|
||||
final JpaRollout rollout = getRolloutAndThrowExceptionIfNotFound(update.getId());
|
||||
|
||||
checkIfDeleted(update.getId(), rollout.getStatus());
|
||||
|
||||
update.getName().ifPresent(rollout::setName);
|
||||
update.getDescription().ifPresent(rollout::setDescription);
|
||||
update.getActionType().ifPresent(rollout::setActionType);
|
||||
@@ -976,6 +1015,11 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
|
||||
|
||||
rollout.setDistributionSet(set);
|
||||
});
|
||||
if (rolloutApprovalStrategy.isApprovalNeeded(rollout)) {
|
||||
rollout.setStatus(RolloutStatus.WAITING_FOR_APPROVAL);
|
||||
rollout.setApprovalDecidedBy(null);
|
||||
rollout.setApprovalRemark(null);
|
||||
}
|
||||
|
||||
return rolloutRepository.save(rollout);
|
||||
}
|
||||
@@ -1067,7 +1111,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
|
||||
/**
|
||||
* Enforces the quota defining the maximum number of {@link Target}s per
|
||||
* {@link RolloutGroup}.
|
||||
*
|
||||
*
|
||||
* @param group
|
||||
* The rollout group
|
||||
* @param requested
|
||||
@@ -1081,7 +1125,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
|
||||
/**
|
||||
* Enforces the quota defining the maximum number of {@link Action}s per
|
||||
* {@link Target}.
|
||||
*
|
||||
*
|
||||
* @param target
|
||||
* The target
|
||||
* @param requested
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.eclipse.hawkbit.repository.PropertiesQuotaManagement;
|
||||
import org.eclipse.hawkbit.repository.QuotaManagement;
|
||||
import org.eclipse.hawkbit.repository.RepositoryDefaultConfiguration;
|
||||
import org.eclipse.hawkbit.repository.RepositoryProperties;
|
||||
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
|
||||
import org.eclipse.hawkbit.repository.RolloutGroupManagement;
|
||||
import org.eclipse.hawkbit.repository.RolloutManagement;
|
||||
import org.eclipse.hawkbit.repository.RolloutStatusCache;
|
||||
@@ -106,6 +107,7 @@ import org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect;
|
||||
import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter;
|
||||
import org.springframework.retry.annotation.EnableRetry;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
import org.springframework.transaction.jta.JtaTransactionManager;
|
||||
@@ -471,7 +473,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
|
||||
* to access quotas
|
||||
* @param properties
|
||||
* JPA properties
|
||||
*
|
||||
*
|
||||
* @return a new {@link TargetFilterQueryManagement}
|
||||
*/
|
||||
@Bean
|
||||
@@ -559,10 +561,25 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
|
||||
final DistributionSetManagement distributionSetManagement, final ApplicationContext context,
|
||||
final ApplicationEventPublisher eventPublisher, final VirtualPropertyReplacer virtualPropertyReplacer,
|
||||
final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry,
|
||||
final JpaProperties properties) {
|
||||
final JpaProperties properties, final RolloutApprovalStrategy rolloutApprovalStrategy) {
|
||||
return new JpaRolloutManagement(targetManagement, deploymentManagement, rolloutGroupManagement,
|
||||
distributionSetManagement, context, eventPublisher, virtualPropertyReplacer, txManager, tenantAware,
|
||||
lockRegistry, properties.getDatabase());
|
||||
lockRegistry, properties.getDatabase(), rolloutApprovalStrategy);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link DefaultRolloutApprovalStrategy} bean.
|
||||
*
|
||||
* @return a new {@link RolloutApprovalStrategy}
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
RolloutApprovalStrategy rolloutApprovalStrategy(final UserDetailsService userDetailsService,
|
||||
final TenantConfigurationManagement tenantConfigurationManagement,
|
||||
final SystemSecurityContext systemSecurityContext) {
|
||||
return new DefaultRolloutApprovalStrategy(userDetailsService, tenantConfigurationManagement,
|
||||
systemSecurityContext);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -88,7 +88,9 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event
|
||||
@ConversionValue(objectValue = "ERROR_CREATING", dataValue = "7"),
|
||||
@ConversionValue(objectValue = "ERROR_STARTING", dataValue = "8"),
|
||||
@ConversionValue(objectValue = "DELETING", dataValue = "9"),
|
||||
@ConversionValue(objectValue = "DELETED", dataValue = "10") })
|
||||
@ConversionValue(objectValue = "DELETED", dataValue = "10"),
|
||||
@ConversionValue(objectValue = "WAITING_FOR_APPROVAL", dataValue = "11"),
|
||||
@ConversionValue(objectValue = "APPROVAL_DENIED", dataValue = "12")})
|
||||
@Convert("rolloutstatus")
|
||||
@NotNull
|
||||
private RolloutStatus status = RolloutStatus.CREATING;
|
||||
@@ -120,6 +122,14 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event
|
||||
@Column(name = "start_at")
|
||||
private Long startAt;
|
||||
|
||||
@Column(name = "approval_decided_by")
|
||||
@Size(min = 1, max = Rollout.APPROVED_BY_MAX_SIZE)
|
||||
private String approvalDecidedBy;
|
||||
|
||||
@Column(name = "approval_remark")
|
||||
@Size(max = Rollout.APPROVAL_REMARK_MAX_SIZE)
|
||||
private String approvalRemark;
|
||||
|
||||
@Transient
|
||||
private transient TotalTargetCountStatus totalTargetCountStatus;
|
||||
|
||||
@@ -271,4 +281,22 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event
|
||||
this.deleted = deleted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApprovalDecidedBy() {
|
||||
return approvalDecidedBy;
|
||||
}
|
||||
|
||||
public void setApprovalDecidedBy(final String approvalDecidedBy) {
|
||||
this.approvalDecidedBy = approvalDecidedBy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApprovalRemark() {
|
||||
return approvalRemark;
|
||||
}
|
||||
|
||||
public void setApprovalRemark(final String approvalRemark) {
|
||||
this.approvalRemark = approvalRemark;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE sp_rollout ADD column approval_decided_by varchar(40);
|
||||
ALTER TABLE sp_rollout ADD column approval_remark varchar(255);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE sp_rollout ADD column approval_decided_by varchar(40);
|
||||
ALTER TABLE sp_rollout ADD column approval_remark varchar(255);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE sp_rollout ADD column approval_decided_by varchar(40);
|
||||
ALTER TABLE sp_rollout ADD column approval_remark varchar(255);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE sp_rollout ADD column approval_decided_by varchar(40);
|
||||
ALTER TABLE sp_rollout ADD column approval_remark varchar(255);
|
||||
@@ -27,6 +27,7 @@ import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.model.TargetTag;
|
||||
import org.eclipse.hawkbit.repository.model.TargetTagAssignmentResult;
|
||||
import org.eclipse.hawkbit.repository.test.util.AbstractIntegrationTest;
|
||||
import org.eclipse.hawkbit.repository.test.util.RolloutTestApprovalStrategy;
|
||||
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
@@ -95,6 +96,9 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest
|
||||
@Autowired
|
||||
protected TenantConfigurationProperties tenantConfigurationProperties;
|
||||
|
||||
@Autowired
|
||||
protected RolloutTestApprovalStrategy approvalStrategy;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
protected List<Action> findActionsByRolloutAndStatus(final Rollout rollout, final Action.Status actionStatus) {
|
||||
return Lists.newArrayList(actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), actionStatus));
|
||||
|
||||
@@ -68,12 +68,17 @@ import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.Expect;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
|
||||
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import ru.yandex.qatools.allure.annotations.Description;
|
||||
import ru.yandex.qatools.allure.annotations.Features;
|
||||
import ru.yandex.qatools.allure.annotations.Step;
|
||||
@@ -87,6 +92,12 @@ import ru.yandex.qatools.allure.annotations.Title;
|
||||
@Stories("Rollout Management")
|
||||
public class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
@Before
|
||||
@After
|
||||
public void reset() {
|
||||
this.approvalStrategy.setApprovalNeeded(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that a running action with distribution-set (A) is not canceled by a rollout which tries to also assign a distribution-set (A)")
|
||||
public void rolloutShouldNotCancelRunningActionWithTheSameDistributionSet() {
|
||||
@@ -1539,6 +1550,59 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Creating a rollout with approval role or approval engine disabled results in the rollout being in " +
|
||||
"READY state.")
|
||||
public void createdRolloutWithApprovalRoleOrApprovalDisabledTransitionsToReadyState() {
|
||||
approvalStrategy.setApprovalNeeded(false);
|
||||
final String successCondition = "50";
|
||||
final String errorCondition = "80";
|
||||
final Rollout rollout = createSimpleTestRolloutWithTargetsAndDistributionSet(10, 10,
|
||||
5, successCondition, errorCondition);
|
||||
assertThat(rollout.getStatus()).isEqualTo(Rollout.RolloutStatus.READY);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Creating a rollout without approver role and approval enabled leads to transition to " +
|
||||
"WAITING_FOR_APPROVAL state.")
|
||||
public void createdRolloutWithoutApprovalRoleTransitionsToWaitingForApprovalState() {
|
||||
approvalStrategy.setApprovalNeeded(true);
|
||||
final String successCondition = "50";
|
||||
final String errorCondition = "80";
|
||||
final Rollout rollout = createSimpleTestRolloutWithTargetsAndDistributionSet(10, 10,
|
||||
5, successCondition, errorCondition);
|
||||
assertThat(rollout.getStatus()).isEqualTo(Rollout.RolloutStatus.WAITING_FOR_APPROVAL);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@Description("Approving a rollout leads to transition to READY state.")
|
||||
public void approvedRolloutTransitionsToReadyState() {
|
||||
approvalStrategy.setApprovalNeeded(true);
|
||||
final String successCondition = "50";
|
||||
final String errorCondition = "80";
|
||||
final Rollout rollout = createSimpleTestRolloutWithTargetsAndDistributionSet(10, 10,
|
||||
5, successCondition, errorCondition);
|
||||
assertThat(rollout.getStatus()).isEqualTo(Rollout.RolloutStatus.WAITING_FOR_APPROVAL);
|
||||
rolloutManagement.approveOrDeny(rollout.getId(), Rollout.ApprovalDecision.APPROVED);
|
||||
final Rollout resultingRollout = rolloutRepository.findOne(rollout.getId());
|
||||
assertThat(resultingRollout.getStatus()).isEqualTo(Rollout.RolloutStatus.READY);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Denying approval for a rollout leads to transition to APPROVAL_DENIED state.")
|
||||
public void deniedRolloutTransitionsToApprovalDeniedState() {
|
||||
approvalStrategy.setApprovalNeeded(true);
|
||||
final String successCondition = "50";
|
||||
final String errorCondition = "80";
|
||||
final Rollout rollout = createSimpleTestRolloutWithTargetsAndDistributionSet(10, 10,
|
||||
5, successCondition, errorCondition);
|
||||
assertThat(rollout.getStatus()).isEqualTo(Rollout.RolloutStatus.WAITING_FOR_APPROVAL);
|
||||
rolloutManagement.approveOrDeny(rollout.getId(), Rollout.ApprovalDecision.DENIED);
|
||||
final Rollout resultingRollout = rolloutRepository.findOne(rollout.getId());
|
||||
assertThat(resultingRollout.getStatus()).isEqualTo(RolloutStatus.APPROVAL_DENIED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExpectEvents({ @Expect(type = RolloutDeletedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
|
||||
@@ -23,11 +23,13 @@ import org.eclipse.hawkbit.cache.DefaultDownloadIdCache;
|
||||
import org.eclipse.hawkbit.cache.DownloadIdCache;
|
||||
import org.eclipse.hawkbit.cache.TenantAwareCacheManager;
|
||||
import org.eclipse.hawkbit.event.BusProtoStuffMessageConverter;
|
||||
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
|
||||
import org.eclipse.hawkbit.repository.RolloutStatusCache;
|
||||
import org.eclipse.hawkbit.repository.event.ApplicationEventFilter;
|
||||
import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
|
||||
import org.eclipse.hawkbit.repository.test.util.RolloutTestApprovalStrategy;
|
||||
import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
|
||||
import org.eclipse.hawkbit.security.DdiSecurityProperties;
|
||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
||||
@@ -128,7 +130,6 @@ public class TestConfiguration implements AsyncConfigurer {
|
||||
TenantAwareCacheManager cacheManager() {
|
||||
return new TenantAwareCacheManager(new GuavaCacheManager(), tenantAware());
|
||||
}
|
||||
|
||||
/**
|
||||
* Bean for the download id cache.
|
||||
*
|
||||
@@ -214,6 +215,11 @@ public class TestConfiguration implements AsyncConfigurer {
|
||||
return serviceMatcher;
|
||||
}
|
||||
|
||||
@Bean
|
||||
RolloutApprovalStrategy rolloutApprovalStrategy() {
|
||||
return new RolloutTestApprovalStrategy();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the protostuff io message converter
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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.test.util;
|
||||
|
||||
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
|
||||
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||
|
||||
/**
|
||||
* Provides an approval strategy that can be manipulated by setting the {link
|
||||
* {@link #approvalNeeded}} flag used for testing.
|
||||
*/
|
||||
public class RolloutTestApprovalStrategy implements RolloutApprovalStrategy {
|
||||
|
||||
private boolean approvalNeeded = false;
|
||||
|
||||
@Override
|
||||
public boolean isApprovalNeeded(Rollout rollout) {
|
||||
return approvalNeeded;
|
||||
}
|
||||
|
||||
public void setApprovalNeeded(boolean approvalNeeded) {
|
||||
this.approvalNeeded = approvalNeeded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApprovalRequired(Rollout rollout) {
|
||||
// do nothing, as no action is needed when testing
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApprovalUser(Rollout rollout) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user