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:
Michael Müller
2018-06-04 16:36:56 +02:00
committed by Dominic Schabel
parent 8a14c931c8
commit cef7c2bbf2
43 changed files with 1056 additions and 43 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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